[
  {
    "path": ".gitattributes",
    "content": "# Set default behavior to automatically normalize line endings\n* text=auto\n\n# Explicitly declare text files you want to always be normalized and converted to native line endings on checkout\n*.dart text eol=lf\n*.yaml text eol=lf\n*.yml text eol=lf\n*.json text eol=lf\n*.xml text eol=lf\n*.html text eol=lf\n*.css text eol=lf\n*.js text eol=lf\n*.md text eol=lf\n*.txt text eol=lf\n*.sh text eol=lf\n*.gradle text eol=lf\n*.properties text eol=lf\n\n# Source code files (C/C++/Objective-C/Swift)\n*.c text eol=lf\n*.cc text eol=lf\n*.cpp text eol=lf\n*.h text eol=lf\n*.hpp text eol=lf\n*.m text eol=lf\n*.mm text eol=lf\n*.swift text eol=lf\n\n# CMake files\n*.cmake text eol=lf\nCMakeLists.txt text eol=lf\n\n# Generated files should always use LF (Flutter generates these)\n**/generated_plugin_registrant.* text eol=lf\n**/generated_plugins.cmake text eol=lf\n**/GeneratedPluginRegistrant.* text eol=lf\n\n# Windows-specific files use CRLF\n*.bat text eol=crlf\n*.cmd text eol=crlf\n*.ps1 text eol=crlf\n\n# Visual Studio files\n*.sln text eol=crlf\n*.vcxproj text eol=crlf\n*.vcxproj.filters text eol=crlf\n\n# Denote all files that are truly binary and should not be modified\n*.png binary\n*.jpg binary\n*.jpeg binary\n*.gif binary\n*.ico binary\n*.ttf binary\n*.otf binary\n*.eot binary\n*.woff binary\n*.woff2 binary\n*.so binary\n*.dylib binary\n*.dll binary\n*.exe binary\n*.jar binary\n*.aar binary\n*.apk binary\n*.aab binary\n*.zip binary\n*.tar binary\n*.gz binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Bug 反馈\ndescription: 提交一个 Bug 反馈。\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        请详细填写以下内容~\n  - type: textarea\n    id: buginfo\n    attributes:\n      label: 在使用的时候发生了什么 Bug ？\n      description: 并且，还请写出您是如何触发这个 Bug 的。\n    validations:\n      required: true\n  - type: dropdown\n    id: os\n    attributes:\n      label: 您在使用哪个操作系统？\n      multiple: false\n      options:\n        - Android\n        - Windows\n        - macOS / iOS\n        - Linux\n    validations:\n      required: true\n  - type: textarea\n    id: osver\n    attributes:\n      label: 请具体提供设备、版本号等信息。\n      description: 例如，“Redmi K40S，Android 13”、“Windows 10 22H2” 等。\n    validations:\n      required: true\n  - type: textarea\n    id: hardware\n    attributes:\n      label: （选填）一些与 Bug 相关的硬件信息。\n      description: （选填）例如，有视频播放问题，可以填写“显卡型号”、“显卡驱动版本”等。\n  - type: textarea\n    id: logs\n    attributes:\n      label: 日志信息\n      description: 请在 “我的 - 关于 - 错误日志” 界面复制错误日志，并粘贴在这里。\n      value: |\n        <details open><summary>Log</summary>\n\n        ```shell\n        [在此处粘贴你的日志]\n        ```\n\n        </details>\n    validations:\n      required: true\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: 提交前确认\n      description: 在提交前，请确认以下内容\n      options:\n        - label: issue 列表中，没有我发现的这个 Bug\n          required: true\n        - label: 我正在使用最新版本的 Kazumi\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other.yml",
    "content": "name: 其他 issue\ndescription: 新功能需求、问题询问等\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        请详细填写以下内容~\n  - type: textarea\n    id: buginfo\n    attributes:\n      label: issue 内容\n      description: 请填写您的 issue 内容。要添加附件，请点击输入框后，直接将附件拖进输入框。\n    validations:\n      required: true\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: 提交前确认\n      description: 在提交前，请确认以下内容\n      options:\n        - label: issue 列表中，没有我的新功能需求 / 问题\n          required: true\n"
  },
  {
    "path": ".github/workflows/pr.yaml",
    "content": "---\n    name: \"PR workflow\"\n\n    on:\n      pull_request:\n        types:\n          - opened\n          - synchronize\n          - reopened\n          - ready_for_review\n        paths-ignore:\n          - 'static/**'\n          - '**.md'\n          - '.gitignore'\n          - '.github/ISSUE_TEMPLATE/**'\n          - 'fastlane/**'\n      workflow_dispatch:\n        inputs:\n          logLevel:\n            description: 'Log level'\n            required: true\n            default: 'warning'\n          signpath_sign:\n            description: 'sign binary by signpath'\n            required: false\n            default: false\n            type: boolean\n          run_android:\n            description: 'manually run android build'\n            required: false\n            default: false\n            type: boolean\n          run_windows:\n            description: 'manually run windows build'\n            required: false\n            default: false\n            type: boolean\n          run_ios:\n            description: 'manually run ios build'\n            required: false\n            default: false\n            type: boolean\n          run_macos:\n            description: 'manually run macos build'\n            required: false\n            default: false\n            type: boolean\n          run_linux:\n            description: 'manually run linux build'\n            required: false\n            default: false\n            type: boolean\n    \n    jobs:\n      changes:\n        if: ${{ ! github.event.pull_request.draft }}\n        runs-on: \"ubuntu-latest\"\n        permissions:\n          pull-requests: read\n        outputs:\n          android: ${{ steps.filter.outputs.android }}\n          windows: ${{ steps.filter.outputs.windows }}\n          ios: ${{ steps.filter.outputs.ios }}\n          macos: ${{ steps.filter.outputs.macos }}\n          linux: ${{ steps.filter.outputs.linux }}\n          all: ${{ steps.filter.outputs.all }}\n        steps:\n          - uses: actions/checkout@v4\n          - uses: dorny/paths-filter@v3\n            id: filter\n            with:\n              predicate-quantifier: 'every'\n              filters: |\n                android:\n                  - 'android/**'\n                windows:\n                  - 'windows/**'\n                ios:\n                  - 'ios/**'\n                macos:\n                  - 'macos/**'\n                linux:\n                  - 'linux/**'\n                all:\n                  - '!android/**'\n                  - '!windows/**'\n                  - '!ios/**'\n                  - '!macos/**'\n                  - '!linux/**'\n\n\n      flutter-build-android:\n        needs: changes\n        if: ${{ github.event.inputs.run_android || (! github.event.pull_request.draft && (needs.changes.outputs.android || needs.changes.outputs.all)) }}\n        name: \"Release for android\"\n        runs-on: \"ubuntu-latest\"\n        permissions: write-all\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Install dependencies\n            run: |\n              sudo apt-get update\n              sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev libasound2-dev\n            shell: bash\n          - name: Set up JDK 17\n            uses: actions/setup-java@v4\n            with:\n              java-version: '17'\n              distribution: 'temurin'          \n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Get Flutter dependencies\n            run: flutter pub get\n            shell: bash\n          - name: Print Flutter version\n            run: flutter doctor -v\n            shell: bash\n          - name: Build Flutter for Android\n            run: flutter build apk --split-per-abi\n            shell: bash\n          - name: Package android build output\n            run: cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk Kazumi_android_canary.apk\n            shell: bash\n\n          - name: Upload android outputs\n            uses: actions/upload-artifact@v4\n            with:\n              name: android_outputs\n              path: Kazumi_android_*.apk\n\n      flutter-build-windows:\n        needs: changes\n        if: ${{ github.event.inputs.run_windows || (! github.event.pull_request.draft && (needs.changes.outputs.windows || needs.changes.outputs.all)) }}\n        name: \"Release for windows\"\n        runs-on: \"windows-latest\"\n        permissions: write-all\n    \n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - run: choco install yq\n          - name: Enable Git longpaths\n            run: git config --system core.longpaths true\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Set up Java\n            uses: actions/setup-java@v4\n            with:\n              distribution: 'temurin'\n              java-version: '18'\n          - run: flutter pub get\n          - run: flutter build windows  \n          - run: Compress-Archive build/windows/x64/runner/Release/* Kazumi_windows_canary.zip\n          - name: Upload windows outputs\n            id: unsigned-windows-packet-artifacts\n            uses: actions/upload-artifact@v4\n            with:\n              name: windows_outputs\n              path: |\n                Kazumi_windows_*.zip\n          # - name: Build unsigned msix\n          #   run: dart run msix:create\n          # - name: Upload windows msix ouputs\n          #   uses: actions/upload-artifact@v4\n          #   id: unsigned-windows-msix-artifacts\n          #   with:\n          #     name: windows_msix_outputs\n          #     path: |\n          #       build/windows/x64/runner/Release/kazumi.msix\n          \n          # - run: New-Item -Path \"build/windows/msix_signed_output\" -ItemType Directory\n          # - name: sign windows msix\n          #   if: ${{ github.event.inputs.signpath_sign == 'true'}}\n          #   uses: signpath/github-action-submit-signing-request@v1\n          #   with:\n          #     api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'\n          #     organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877'\n          #     project-slug: 'Kazumi'\n          #     signing-policy-slug: 'test-signing'\n          #     artifact-configuration-slug: 'MSIX'\n          #     github-artifact-id: '${{ steps.unsigned-windows-msix-artifacts.outputs.artifact-id }}'\n          #     wait-for-completion: true\n          #     output-artifact-directory: 'build/windows/msix_signed_output'\n          \n          # - name: Upload windows msix signed ouputs\n          #   if: ${{ github.event.inputs.signpath_sign == 'true'}}\n          #   uses: actions/upload-artifact@v4\n          #   id: signed-windows-msix-artifacts\n          #   with:\n          #     name: windows_msix_signed_outputs\n          #     path: build/windows/msix_signed_output/*.msix\n\n\n      flutter-build-ios:\n        needs: changes\n        if: ${{ github.event.inputs.run_ios || (! github.event.pull_request.draft && (needs.changes.outputs.ios || needs.changes.outputs.all)) }}\n        name: \"Release for iOS\"\n        runs-on: \"macos-latest\"\n        permissions: write-all\n\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - run: flutter pub get\n          - name: Build IPA\n            run: |\n              flutter build ios --release --no-codesign\n          - name: Create IPA\n            run: |\n              mkdir Payload\n              cp -R build/ios/iphoneos/Runner.app Payload/Runner.app\n              find Payload/Runner.app/Frameworks -type d -name \"*.framework\" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \\;\n              zip -q -r Kazumi_ios_canary_no_sign.ipa Payload\n          - name: Upload iOS build\n            uses: actions/upload-artifact@v4\n            with:\n              name: ios_outputs\n              path: Kazumi_ios_*.ipa\n\n      flutter-build-macos:\n        needs: changes\n        if: ${{ github.event.inputs.run_macos || (! github.event.pull_request.draft && (needs.changes.outputs.macos || needs.changes.outputs.all)) }}\n        name: \"Release for Macos\"\n        runs-on: \"macos-latest\"\n        permissions: write-all\n\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - run: flutter pub get\n          - run: flutter build macos --release\n          - name: Create DMG\n            run: |\n              npm install --global create-dmg\n              create-dmg build/macos/Build/Products/Release/Kazumi.app\n            continue-on-error: true\n          - name: Rename DMG\n            run: mv Kazumi*.dmg Kazumi_macos_canary.dmg\n          - name: Upload MacOS build\n            uses: actions/upload-artifact@v4\n            with:\n              name: macos_outputs\n              path: Kazumi_macos_*.dmg\n\n      flutter-build-linux:\n        needs: changes\n        if: ${{ github.event.inputs.run_linux || (! github.event.pull_request.draft && (needs.changes.outputs.linux || needs.changes.outputs.all)) }}\n        name: \"Release for Linux\"\n        runs-on: \"ubuntu-latest\" \n        permissions: write-all\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Install dependencies\n            run: |\n              sudo apt-get update\n              sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev\n              sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \\\n                  libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \\\n                  libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \\\n                  libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \\\n                  libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \\\n                  libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \\\n                  libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \\\n                  libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \\\n                  libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \\\n                  libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \\\n                  linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \\\n                  x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \\\n                  unixodbc-dev libpq-dev libxxhash-dev libaom-dev \n            shell: bash\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Get Flutter dependencies\n            run: flutter pub get\n            shell: bash\n          - name: Build Flutter for Linux\n            run: flutter build linux\n            shell: bash\n          # - name: Download FFmpeg Assets\n          #   uses: dsaltares/fetch-gh-release-asset@master\n          #   with:\n          #     repo: 'Predidit/avbuild'\n          #     version: 'tags/1.1.0'\n          #     file: 'ffmpeg_linux_amd64.zip'\n          #     token: ${{ secrets.GITHUB_TOKEN }}\n          # - run: rm -f build/linux/x64/release/bundle/lib/libffmpeg.so.7\n          # - run: unzip ffmpeg_linux_amd64.zip -d build/linux/x64/release/bundle/lib\n          - name: Package linux build output\n            run: |\n              # Tarball package\n              tar -zcvf Kazumi_linux_canary.tar.gz -C build/linux/x64/release/bundle .\n\n              # Debian package\n              mkdir Kazumi_linux_canary_amd64\n              cd Kazumi_linux_canary_amd64\n              mkdir -p opt/Kazumi\n              mkdir -p usr/share/applications\n              mkdir -p usr/share/icons/hicolor/512x512/apps\n              cp -r ../build/linux/x64/release/bundle/* opt/Kazumi\n              cp -r ../assets/linux/DEBIAN .\n              chmod 0755 DEBIAN/postinst\n              chmod 0755 DEBIAN/postrm\n\n              cat>DEBIAN/control<<EOF\n              Maintainer: madoka773 <valigarmanda55@gmail.com>\n              Package: Kazumi\n              Version: 0.0.1\n              Section: x11\n              Priority: optional\n              Architecture: amd64\n              Essential: no\n              Installed-Size: 34648\n              Description: Watch Animes online with danmaku support.\n              Homepage: https://github.com/Predidit/Kazumi\n              Depends: libayatana-appindicator3-1,\n                       gir1.2-ayatanaappindicator3-0.1,\n                       libwebkit2gtk-4.1-0\n              EOF\n\n              cp ../assets/linux/io.github.Predidit.Kazumi.desktop usr/share/applications\n              cp ../assets/images/logo/logo_linux.png usr/share/icons/hicolor/512x512/apps/io.github.Predidit.Kazumi.png\n\n              cd ..\n              dpkg-deb --build --root-owner-group Kazumi_linux_canary_amd64\n            shell: bash\n\n          - name: Upload linux outputs\n            uses: actions/upload-artifact@v4\n            with:\n              name: linux_outputs\n              path: |\n                Kazumi_linux_*.tar.gz\n                Kazumi_linux_*.deb\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "---\n    name: \"release\"\n    \n    on:\n      push:\n        tags:\n          - \"*\"\n      workflow_dispatch:\n        inputs:\n          logLevel:\n            description: 'Log level'     \n            required: true\n            default: 'warning'\n    \n    jobs:\n      flutter-build-android:\n        name: \"Release for android\"\n        runs-on: \"ubuntu-latest\" \n        permissions: write-all\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Extract tag name\n            run: echo \"tag=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n            shell: bash\n          - name: Echo build progress\n            run: echo \"Kazumi_android_${{ env.tag }}.apk build progress\"\n            shell: bash\n          - name: Install dependencies\n            run: |\n              sudo apt-get update\n              sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev libasound2-dev\n            shell: bash\n          - name: Set up JDK 17\n            uses: actions/setup-java@v4\n            with:\n              java-version: '17'\n              distribution: 'temurin'          \n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Get Flutter dependencies\n            run: flutter pub get\n            shell: bash\n          - name: Inject DanDan API Credentials\n            run: |\n              sed -i \"s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g\" lib/utils/mortis.dart\n              sed -i \"s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g\" lib/utils/mortis.dart\n          - name: Build Flutter for Android\n            run: flutter build apk --split-per-abi\n            shell: bash\n          - name: Package android build output\n            run: cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk Kazumi_android_${env:tag}.apk\n            shell: bash\n\n          - name: Upload android outputs\n            uses: actions/upload-artifact@v4\n            with:\n              name: android_outputs\n              path: Kazumi_android_*.apk\n\n      flutter-build-windows:\n        name: \"Release for windows\"\n        runs-on: \"windows-latest\"\n        needs: [flutter-build-android, flutter-build-ios, flutter-build-linux, flutter-build-macos] \n        permissions: write-all\n    \n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - run: |\n                  $tag = \"${{ github.ref }}\".Replace('refs/tags/', '')\n                  echo \"tag=$(echo $tag)\" >> $env:GITHUB_ENV\n          - run: echo \"Kazumi_windows_${env:tag}.zip build progress\"\n          - run: choco install yq\n          - name: Enable Git longpaths\n            run: git config --system core.longpaths true\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Set up Java\n            uses: actions/setup-java@v4\n            with:\n              distribution: 'temurin'\n              java-version: '18'\n          - run: flutter pub get\n          - name: Inject DanDan API Credentials\n            env:\n              DANDANAPI_APPID: ${{ secrets.DANDANAPI_APPID }}\n              DANDANAPI_KEY: ${{ secrets.DANDANAPI_KEY }}\n            run: |\n              (Get-Content -Path 'lib/utils/mortis.dart') -replace \"kvpx7qkqjh\", \"$env:DANDANAPI_APPID\" | Set-Content -Path 'lib/utils/mortis.dart'\n              (Get-Content -Path 'lib/utils/mortis.dart') -replace \"rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax\", \"$env:DANDANAPI_KEY\" | Set-Content -Path 'lib/utils/mortis.dart'\n          - run: flutter build windows   \n          - run: Compress-Archive build/windows/x64/runner/Release/* Kazumi_windows_${env:tag}.zip\n          - name: Upload windows outputs\n            uses: actions/upload-artifact@v4\n            id: unsigned-windows-zip-artifacts\n            with:\n              name: windows_outputs\n              path: |\n                Kazumi_windows_*.zip\n\n          # Sign Zip\n          - run: New-Item -Path \"build/windows/zip_signed_output\" -ItemType Directory\n          - name: sign windows zip\n            uses: signpath/github-action-submit-signing-request@v1.1\n            with:\n              api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'\n              organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877'\n              project-slug: 'Kazumi'\n              signing-policy-slug: 'release-signing'\n              artifact-configuration-slug: 'Packet'\n              github-artifact-id: '${{ steps.unsigned-windows-zip-artifacts.outputs.artifact-id }}'\n              wait-for-completion: true\n              output-artifact-directory: 'build/windows/zip_signed_output'\n          \n          - name: Upload windows zip signed ouputs\n            uses: actions/upload-artifact@v4\n            id: signed-windows-zip-artifacts\n            with:\n              name: windows_zip_signed_outputs\n              path: build/windows/zip_signed_output/*.zip\n              \n          # Replace Unpacked Artifact with Signed Artifact\n          - name: Replace Unpacked Artifact with Signed Artifact\n            run: Expand-Archive -Path \"build/windows/zip_signed_output/Kazumi_windows_${env:tag}.zip\" -DestinationPath \"build/windows/x64/runner/Release\" -Force\n          \n          # Build Unsigned MSIX\n          - name: Build unsigned msix\n            run: dart run msix:create\n          - name: Upload windows msix ouputs\n            uses: actions/upload-artifact@v4\n            id: unsigned-windows-msix-artifacts\n            with:\n              name: windows_msix_outputs\n              path: |\n                build/windows/x64/runner/Release/kazumi.msix\n          \n          # Sign MSIX\n          - run: New-Item -Path \"build/windows/msix_signed_output\" -ItemType Directory\n          - name: sign windows msix\n            uses: signpath/github-action-submit-signing-request@v1.1\n            with:\n              api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'\n              organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877'\n              project-slug: 'Kazumi'\n              signing-policy-slug: 'release-signing'\n              artifact-configuration-slug: 'MSIX'\n              github-artifact-id: '${{ steps.unsigned-windows-msix-artifacts.outputs.artifact-id }}'\n              wait-for-completion: true\n              output-artifact-directory: 'build/windows/msix_signed_output'\n          \n          - name: Upload windows msix signed ouputs\n            uses: actions/upload-artifact@v4\n            id: signed-windows-msix-artifacts\n            with:\n              name: windows_msix_signed_outputs\n              path: build/windows/msix_signed_output/*.msix\n\n      flutter-build-linux:\n        name: \"Release for Linux\"\n        runs-on: \"ubuntu-latest\" \n        permissions: write-all\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Extract tag name\n            run: echo \"tag=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n            shell: bash\n          - name: Echo build progress\n            run: echo \"Kazumi_linux_${{ env.tag }}.tar.gz build progress\"\n            shell: bash\n          - name: Install dependencies\n            run: |\n              sudo apt-get update\n              sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev\n              sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \\\n                  libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \\\n                  libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \\\n                  libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \\\n                  libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \\\n                  libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \\\n                  libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \\\n                  libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \\\n                  libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \\\n                  libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \\\n                  linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \\\n                  x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \\\n                  unixodbc-dev libpq-dev libxxhash-dev libaom-dev              \n            shell: bash\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - name: Get Flutter dependencies\n            run: flutter pub get\n            shell: bash\n          - name: Inject DanDan API Credentials\n            run: |\n              sed -i \"s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g\" lib/utils/mortis.dart\n              sed -i \"s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g\" lib/utils/mortis.dart\n          - name: Build Flutter for Linux\n            run: flutter build linux\n            shell: bash\n          # - name: Download FFmpeg Assets\n          #   uses: dsaltares/fetch-gh-release-asset@master\n          #   with:\n          #     repo: 'Predidit/avbuild'\n          #     version: 'tags/1.1.0'\n          #     file: 'ffmpeg_linux_amd64.zip'\n          #     token: ${{ secrets.GITHUB_TOKEN }}\n          # - run: rm -f build/linux/x64/release/bundle/lib/libffmpeg.so.7\n          # - run: unzip ffmpeg_linux_amd64.zip -d build/linux/x64/release/bundle/lib\n          - name: Package linux build output\n            run: |\n              # Tarball package\n              tar -zcvf Kazumi_linux_${{ env.tag }}_amd64.tar.gz -C build/linux/x64/release/bundle .\n\n              # Debian package\n              mkdir Kazumi_linux_${{ env.tag }}_amd64\n              cd Kazumi_linux_${{ env.tag }}_amd64\n              mkdir -p opt/Kazumi\n              mkdir -p usr/share/applications\n              mkdir -p usr/share/icons/hicolor/512x512/apps\n              cp -r ../build/linux/x64/release/bundle/* opt/Kazumi\n              cp -r ../assets/linux/DEBIAN .\n              chmod 0755 DEBIAN/postinst\n              chmod 0755 DEBIAN/postrm\n\n              cat>DEBIAN/control<<EOF\n              Maintainer: madoka773 <valigarmanda55@gmail.com>\n              Package: Kazumi\n              Version: ${{ env.tag }}\n              Section: x11\n              Priority: optional\n              Architecture: amd64\n              Essential: no\n              Installed-Size: 34648\n              Description: Watch Animes online with danmaku support.\n              Homepage: https://github.com/Predidit/Kazumi\n              Depends: libayatana-appindicator3-1,\n                       gir1.2-ayatanaappindicator3-0.1,\n                       libwebkit2gtk-4.1-0\n              EOF\n\n              cp ../assets/linux/io.github.Predidit.Kazumi.desktop usr/share/applications\n              cp ../assets/images/logo/logo_linux.png usr/share/icons/hicolor/512x512/apps/io.github.Predidit.Kazumi.png\n\n              cd ..\n              dpkg-deb --build --root-owner-group Kazumi_linux_${{ env.tag }}_amd64\n            shell: bash\n\n          - name: Upload linux outputs\n            uses: actions/upload-artifact@v4\n            with:\n              name: linux_outputs\n              path: |\n                Kazumi_linux_*.tar.gz\n                Kazumi_linux_*.deb\n\n      flutter-build-ios:\n        name: \"Release for iOS\"\n        runs-on: \"macos-latest\"\n        permissions: write-all\n\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Extract tag name\n            run: echo \"tag=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n          - name: Echo build progress\n            run: echo \"Kazumi_ios_${{ env.tag }}.ipa build progress\"\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - run: flutter pub get\n          - name: Inject DanDan API Credentials\n            run: |\n              sed -i '' \"s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g\" lib/utils/mortis.dart\n              sed -i '' \"s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g\" lib/utils/mortis.dart\n          - name: Build IPA\n            run: |\n              flutter build ios --release --no-codesign\n          - name: Create IPA\n            run: |\n              mkdir Payload\n              cp -R build/ios/iphoneos/Runner.app Payload/Runner.app\n              find Payload/Runner.app/Frameworks -type d -name \"*.framework\" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \\;\n              zip -q -r Kazumi_ios_${{ env.tag }}_no_sign.ipa Payload\n          - name: Upload iOS build\n            uses: actions/upload-artifact@v4\n            with:\n              name: ios_outputs\n              path: Kazumi_ios_*.ipa\n\n      flutter-build-macos:\n        name: \"Release for Macos\"\n        runs-on: \"macos-latest\"\n        permissions: write-all\n\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Extract tag name\n            run: echo \"tag=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n          - name: Echo build progress\n            run: echo \"Kazumi_macos_${{ env.tag }}.dmg build progress\"\n          - name: Set up Flutter\n            uses: subosito/flutter-action@v2.16.0\n            with:\n              channel: stable\n              flutter-version-file: pubspec.yaml\n          - run: flutter pub get\n          - name: Inject DanDan API Credentials\n            run: |\n              sed -i '' \"s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g\" lib/utils/mortis.dart\n              sed -i '' \"s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g\" lib/utils/mortis.dart\n          - run: flutter build macos --release\n          - name: Create DMG\n            run: |\n              npm install --global create-dmg\n              create-dmg build/macos/Build/Products/Release/Kazumi.app\n            continue-on-error: true\n          - name: Rename DMG\n            run: mv Kazumi*.dmg Kazumi_macos_${{ env.tag }}.dmg\n          - name: Upload MacOS build\n            uses: actions/upload-artifact@v4\n            with:\n              name: macos_outputs\n              path: Kazumi_macos_*.dmg\n\n      release:\n        name: \"Release\"\n        runs-on: \"ubuntu-latest\"\n        needs: [flutter-build-windows] \n        permissions: write-all\n        steps:\n          - name: Clone repository\n            uses: actions/checkout@v4\n          - name: Extract tag name\n            run: echo \"tag=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n            shell: bash\n          - name: Set up JDK 17\n            uses: actions/setup-java@v4\n            with:\n              java-version: '17'\n              distribution: 'temurin'          \n          - name: Setup Android SDK\n            uses: android-actions/setup-android@v3\n          - name: Setup Android build tools\n            run: sdkmanager \"build-tools;34.0.0\"\n  \n          - name: Download windows zip build file\n            uses: actions/download-artifact@v4\n            with:\n              name: windows_zip_signed_outputs\n              path: windows_zip_signed_outputs \n          - name: List files in windows_outputs directory\n            run: ls -l windows_zip_signed_outputs   \n          - name: Copy windows build file to root\n            run: cp windows_zip_signed_outputs/* Kazumi_windows_${{ env.tag }}.zip\n\n          - name: Download windows msix build file\n            uses: actions/download-artifact@v4\n            with:\n              name: windows_msix_signed_outputs\n              path: windows_msix_signed_outputs  \n          - name: List files in windows_msix_signed_outputs directory\n            run: ls -l windows_msix_signed_outputs   \n          - name: Copy windows build file to root\n            run: cp windows_msix_signed_outputs/* Kazumi_windows_${{ env.tag }}.msix\n\n          - name: Download android build file\n            uses: actions/download-artifact@v4\n            with:\n              name: android_outputs\n              path: android_outputs \n          - name: List files in android_outputs directory\n            run: ls -l android_outputs   \n          - name: Copy android build file to unsigned floder\n            run: | \n              mkdir build\n              mkdir build/unsigned\n              mkdir build/signed\n              cp android_outputs/* build/unsigned/Kazumi_android_${{ env.tag }}.apk\n\n          - name: Download iOS build file\n            uses: actions/download-artifact@v4\n            with:\n              name: ios_outputs\n              path: ios_outputs  \n          - name: List files in ios_outputs directory\n            run: ls -l ios_outputs   \n          - name: Copy ios build file to root\n            run: cp ios_outputs/* Kazumi_ios_${{ env.tag }}_no_sign.ipa\n\n          - name: Download macos build file\n            uses: actions/download-artifact@v4\n            with:\n              name: macos_outputs\n              path: macos_outputs  \n          - name: List files in macos_outputs directory\n            run: ls -l macos_outputs   \n          - name: Copy macos build file to root\n            run: cp macos_outputs/* Kazumi_macos_${{ env.tag }}.dmg   \n            \n          - name: Download linux build file\n            uses: actions/download-artifact@v4\n            with:\n              name: linux_outputs\n              path: linux_outputs  \n          - name: List files in linux_outputs directory\n            run: ls -l linux_outputs   \n          - name: Copy linux build file to root\n            run: cp linux_outputs/* .\n\n          - name: Sign APK\n            id: sign_app\n            uses: filippoLeporati93/android-release-signer@v1\n            with:\n              releaseDirectory: build/unsigned\n              signingKeyBase64: ${{ secrets.SIGNING_KEY_BASE64 }}\n              alias: ${{ secrets.KEY_ALIAS }}\n              keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}\n            env:\n              BUILD_TOOLS_VERSION: \"34.0.0\"\n\n          - name: Copy Signed android build file\n            run: cp ${{steps.sign_app.outputs.signedReleaseFile}} build/signed/Kazumi_android_${{ env.tag }}.apk\n\n          - name: Create release\n            uses: softprops/action-gh-release@v2\n            with:\n              files: |\n                build/signed/*.apk\n                Kazumi_windows_*.zip\n                Kazumi_windows_*.msix\n                Kazumi_macos_*.dmg\n                Kazumi_ios_*.ipa\n                Kazumi_linux_*.tar.gz\n                Kazumi_linux_*.deb\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\n# Added after flutter 3.29\n/android/app/.cxx/\nandroid/build/reports/problems/\n\n# CocoaPods - iOS/macOS dependencies\n# Podfile.lock is auto-generated by Flutter, no need to commit\n**/ios/Pods/\n**/macos/Pods/\n**/ios/Podfile.lock\n**/macos/Podfile.lock\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"fastlane/.flutter\"]\n\tpath = fastlane/.flutter\n\turl = https://github.com/flutter/flutter.git\n\tbranch = stable\n[submodule \"fastlane/.libmpv-android-video-build\"]\n\tpath = fastlane/.libmpv-android-video-build\n\turl = https://github.com/Predidit/libmpv-android-video-build.git\n"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"54e66469a933b60ddf175f858f82eaeb97e48c8d\"\n  channel: \"stable\"\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: android\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: ios\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: linux\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: macos\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: web\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: windows\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cmake.ignoreCMakeListsMissing\": true,\n    \"search.exclude\": {\n        \"**/fastlane\": true\n    },\n    \"dart.analysisExcludedFolders\": [\n        \"**/fastlane/**\"\n    ],\n    \"files.associations\": {\n        \"xiosbase\": \"cpp\",\n        \"utility\": \"cpp\",\n        \"xstring\": \"cpp\",\n        \"xtree\": \"cpp\",\n        \"algorithm\": \"cpp\",\n        \"any\": \"cpp\",\n        \"array\": \"cpp\",\n        \"atomic\": \"cpp\",\n        \"bit\": \"cpp\",\n        \"cctype\": \"cpp\",\n        \"charconv\": \"cpp\",\n        \"chrono\": \"cpp\",\n        \"cinttypes\": \"cpp\",\n        \"clocale\": \"cpp\",\n        \"cmath\": \"cpp\",\n        \"codecvt\": \"cpp\",\n        \"compare\": \"cpp\",\n        \"concepts\": \"cpp\",\n        \"condition_variable\": \"cpp\",\n        \"coroutine\": \"cpp\",\n        \"cstddef\": \"cpp\",\n        \"cstdint\": \"cpp\",\n        \"cstdio\": \"cpp\",\n        \"cstdlib\": \"cpp\",\n        \"cstring\": \"cpp\",\n        \"ctime\": \"cpp\",\n        \"cwchar\": \"cpp\",\n        \"exception\": \"cpp\",\n        \"filesystem\": \"cpp\",\n        \"format\": \"cpp\",\n        \"forward_list\": \"cpp\",\n        \"functional\": \"cpp\",\n        \"future\": \"cpp\",\n        \"initializer_list\": \"cpp\",\n        \"iomanip\": \"cpp\",\n        \"ios\": \"cpp\",\n        \"iosfwd\": \"cpp\",\n        \"iostream\": \"cpp\",\n        \"istream\": \"cpp\",\n        \"iterator\": \"cpp\",\n        \"limits\": \"cpp\",\n        \"list\": \"cpp\",\n        \"locale\": \"cpp\",\n        \"map\": \"cpp\",\n        \"memory\": \"cpp\",\n        \"mutex\": \"cpp\",\n        \"new\": \"cpp\",\n        \"optional\": \"cpp\",\n        \"ostream\": \"cpp\",\n        \"ratio\": \"cpp\",\n        \"set\": \"cpp\",\n        \"sstream\": \"cpp\",\n        \"stdexcept\": \"cpp\",\n        \"stop_token\": \"cpp\",\n        \"streambuf\": \"cpp\",\n        \"string\": \"cpp\",\n        \"system_error\": \"cpp\",\n        \"thread\": \"cpp\",\n        \"tuple\": \"cpp\",\n        \"type_traits\": \"cpp\",\n        \"typeinfo\": \"cpp\",\n        \"unordered_map\": \"cpp\",\n        \"variant\": \"cpp\",\n        \"vector\": \"cpp\",\n        \"xfacet\": \"cpp\",\n        \"xhash\": \"cpp\",\n        \"xlocale\": \"cpp\",\n        \"xlocbuf\": \"cpp\",\n        \"xlocinfo\": \"cpp\",\n        \"xlocmes\": \"cpp\",\n        \"xlocmon\": \"cpp\",\n        \"xlocnum\": \"cpp\",\n        \"xloctime\": \"cpp\",\n        \"xmemory\": \"cpp\",\n        \"xtr1common\": \"cpp\",\n        \"xutility\": \"cpp\"\n    }\n}"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# Kazumi\n使用 Flutter 开发的基于自定义规则的番剧采集与在线观看程序。使用最多五行基于 `Xpath` 语法的选择器构建自己的规则。支持规则导入与规则分享。支持基于 `Anime4K` 的实时超分辨率。绝赞开发中 (～￣▽￣)～\n\n## 支持平台\n\n- Android 10 及以上\n- Windows 10 及以上\n- MacOS 10.15 及以上\n- Linux (实验性)\n- iOS 13 及以上 (需要[自签名](https://kazumi.app/docs/misc/how-to-install-in-ios.html))\n- HarmonyOS 5.0 及以上 (位于[分支仓库](https://github.com/ErBWs/Kazumi/releases/latest)，需要[侧载](https://kazumi.app/docs/misc/how-to-install-in-ohos.html))\n\n## 屏幕截图 \n\n<table>\n  <tr>\n    <td><img alt=\"\" src=\"static/screenshot/img_1.png\"></td>\n    <td><img alt=\"\" src=\"static/screenshot/img_2.png\"></td>\n    <td><img alt=\"\" src=\"static/screenshot/img_3.png\"></td>\n  <tr>\n  <tr>\n    <td><img alt=\"\" src=\"static/screenshot/img_4.png\"></td>\n    <td><img alt=\"\" src=\"static/screenshot/img_5.png\"></td>\n    <td><img alt=\"\" src=\"static/screenshot/img_6.png\"></td>\n  <tr>\n</table>\n\n## 功能 / 开发计划\n\n- [x] 规则编辑器\n- [x] 番剧目录\n- [x] 番剧搜索\n- [x] 番剧时间表\n- [x] 番剧字幕\n- [x] 分集播放\n- [x] 视频播放器\n- [x] 多视频源支持\n- [x] 规则分享\n- [x] 硬件加速\n- [x] 高刷适配\n- [x] 追番列表\n- [x] 番剧弹幕\n- [x] 在线更新\n- [x] 历史记录\n- [x] 倍速播放\n- [x] 配色方案 \n- [x] 跨设备同步\n- [x] 无线投屏 (DLNA)\n- [x] 外部播放器播放\n- [x] 超分辨率\n- [x] 一起看\n- [ ] 番剧下载\n- [ ] 番剧更新提醒\n- [ ] 还有更多 (/・ω・＼) \n\n## 下载\n\n通过本页面 [releases](https://github.com/Predidit/Kazumi/releases) 选项卡下载：\n\n<a href=\"https://github.com/Predidit/Kazumi/releases\">\n  <img src=\"static/svg/get_it_on_github.svg\" alt=\"Get it on Github\" width=\"200\"/>\n</a>\n\n### Android\n\n<a href=\"https://f-droid.org/packages/com.predidit.kazumi\">\n  <img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on-en-us.svg\"\n  alt=\"Get it on F-Droid\" width=\"200\">\n</a>\n\n### GNU/Linux\n\n&nbsp;&nbsp;\n<a href=\"https://flathub.org/apps/io.github.Predidit.Kazumi\">\n  <img src=\"https://flathub.org/api/badge?svg&locale=en\" alt=\"Get it on Flathub\" width=\"175\"/>\n</a>\n\n#### Arch Linux\n\n可以从 [AUR](http://aur.archlinux.org) 或 [archlinuxcn](https://github.com/archlinuxcn/repo) 安装。\n\n##### AUR\n\n```bash\n[yay/paru] -S kazumi # 从源码构建\n[yay/paru] -S kazumi-bin # 二进制包\n```\n\n##### archlinuxcn\n\n```bash\nsudo pacman -S kazumi\n```\n\n## 贡献\n\n欢迎向我们的 [规则仓库](https://github.com/Predidit/KazumiRules) 提交您的自定义规则。您可以自由选择是否在规则中留下您的ID\n\n## Q&A\n\n<details>\n<summary>使用者 Q&A</summary>\n\n#### Q: 为什么少数番剧中有广告？\n\nA: 本项目未插入任何广告。广告来自视频源, 请不要相信广告中的任何内容, 并尽量选择没有广告的视频源观看。\n\n#### Q: 为什么我启用超分辨率功能后播放卡顿？\n\nA: 超分辨率功能对 GPU 性能要求较高, 如果没有在高性能独立显卡上运行 Kazumi, 尽量选择效率档而非质量档。对低分辨率视频源而非高分辨率视频源使用超分也可以降低性能消耗。\n\n#### Q: 为什么播放视频时内存占用较高？\n\nA: 本程序在视频播放时, 会尽可能多地缓存视频到内存, 以提供较好的观看体验。如果您的内存较为紧张, 可以在播放设置选项卡启用低内存模式, 这将限制缓存。\n\n#### Q: 为什么少数番剧无法通过外部播放器观看？\n\nA: 部分视频源的番剧使用了反盗链措施, 这可以被 Kazumi 解决, 但无法被外部播放器解决。\n\n#### Q: 为什么下载的 Linux 版本缺少图标和托盘功能？\n\nA: 使用 .deb 版本进行安装, tar.gz 版本仅为方便二次打包, 这一格式先天缺乏图标和托盘功能支持。\n\n</details>\n\n<details>\n<summary>规则编写者 Q&A</summary>\n\n#### Q: 为什么我的自定义规则无法实现检索？\n\nA: 目前我们对 `Xpath` 语法的支持并不完整, 我们目前只支持以 `//` 开头的选择器。建议参照我们给出的示例规则构建自定义规则。\n\n#### Q: 为什么我的自定义规则可以实现检索, 但不能实现观看？\n\nA: 尝试关闭自定义规则的使用内置播放器选项, 这将尝试使用 `webview` 进行播放, 提高兼容性。但在内置播放器可用时, 建议启用内置播放器, 以获得更加流畅并带有弹幕的观看体验。\n\n</details>\n\n<details>\n<summary>开发者 Q&A</summary>\n\n#### Q: 我在尝试自行编译该项目, 但编译没有成功。\n\nA: 本项目编译需要良好的网络环境, 除了由 Google 托管的 Flutter 相关依赖外, 本项目同样依赖托管在 MavenCentral/Github/SourceForge 上的资源。如果您位于中国大陆, 可能需要设置恰当的镜像地址。\n\n</details>\n\n## 美术资源\n\n本项目图标来自 [Yuquanaaa](https://www.pixiv.net/users/66219277) 发表在 [Pixiv](https://www.pixiv.net/artworks/116666979) 上的作品。\n\n此图标由其原作者 [Yuquanaaa](https://www.pixiv.net/users/66219277) 拥有版权。我们已获得原作者的授权和许可, 可以在本项目中使用这一图标。这一图标不是自由使用的, 未经原作者明确授权, 任何人不得擅自使用、复制、修改或分发这一图标。\n\n本项目内嵌字体为 [Mi Sans](https://hyperos.mi.com/font/en/details/sc/) 字体, 由 [Xiaomi](https://www.mi.com/) 开发和拥有版权。\n\n## 免责声明\n\n本项目基于 GNU 通用公共许可证第 3 版（GPL-3.0）授权。我们不对其适用性、可靠性或准确性作出任何明示或暗示的保证。在法律允许的最大范围内, 作者和贡献者不承担任何因使用本软件而产生的直接、间接、偶然、特殊或后果性的损害赔偿责任。\n\n使用本项目需遵守所在地法律法规, 不得进行任何侵犯第三方知识产权的行为。因使用本项目而产生的数据和缓存应在24小时内清除, 超出 24 小时的使用需获得相关权利人的授权。\n\n## 隐私政策 (Privacy policy)\n\n我们不收集任何用户数据, 不使用任何遥测组件。\n\n## 代码签名策略 (Code signing policy)\n提交者: [Contributors](https://github.com/Predidit/Kazumi/graphs/contributors)\n审阅者: [Owner](https://github.com/Predidit)\n\n## 赞助 (Sponsors)\n| ![signpath](https://signpath.org/assets/favicon-50x50.png) | Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certficate by [SignPath Foundation](https://signpath.org/) |\n|------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|\n\n## 致谢\n\n特别感谢 [XpathSelector](https://github.com/simonkimi/xpath_selector) 这个优秀的项目是本项目的基石。\n\n特别感谢 [DandanPlayer](https://www.dandanplay.com/) 本项目使用了 dandanplayer 开放 API 以提供弹幕交互。\n\n特别感谢 [Bangumi](https://bangumi.tv/) 本项目使用了 Bangumi 开放 API 以提供番剧元数据。\n\n特别感谢 [Anime4K](https://github.com/bloc97/Anime4K) 本项目使用 Anime4K 进行实时超分。\n\n特别感谢 [SyncPlay](https://github.com/Syncplay/syncplay) 本项目使用 SyncPlay 协议并通过 SyncPlay 公共服务器实现一起看功能。\n\n感谢 [media-kit](https://github.com/media-kit/media-kit) 本项目跨平台媒体播放能力来自 media-kit。\n\n感谢 [avbuild](https://github.com/wang-bin/avbuild) 本项目使用了来自 avbuild 的树外补丁实现非标准视频流播放。\n\n感谢 [hive](https://github.com/isar/hive) 本项目持久化储存能力来自 hive。\n\n\n\n\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at https://dart.dev/lints.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "plugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n    id \"dev.flutter.flutter-gradle-plugin\"\n}\n\ndef localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\nandroid {\n    namespace \"com.example.kazumi\"\n    compileSdk flutter.compileSdkVersion\n    // Pin ndk version to 21.3.6528147 to fix android build warning\n    // The build warning throws by gradle plugin, when the ndk is older than gradle plugin required version\n    ndkVersion \"27.2.12479018\"\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n    kotlinOptions {\n        jvmTarget = '17'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"com.predidit.kazumi\"\n        // You can update the following values to match your application needs.\n        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.\n        minSdkVersion flutter.minSdkVersion\n        targetSdkVersion flutter.targetSdkVersion\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.debug\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {}\n\n// F-droid splits APKs by ABI, and requires different versionCode for each ABI.\n// For flutter version X.Y.Z, version code is X0Y0ZA, where A is the ABI code.\n// See:\n// * https://developer.android.com/build/gradle-tips\n// * https://developer.android.com/studio/build/configure-apk-splits\n// * https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/im.nfc.nfsee.yml\next.abiCodes = [\"armeabi-v7a\": 1, \"arm64-v8a\": 2, \"x86_64\": 4]\nimport com.android.build.OutputFile\nandroid.applicationVariants.all { variant ->\n    variant.outputs.each { output ->\n        def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))\n        if (abiVersionCode != null) {\n            output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application\n        android:label=\"Kazumi\"\n        android:name=\"${applicationName}\"\n        android:usesCleartextTraffic=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:extractNativeLibs=\"true\">\n        <meta-data\n             android:name=\"io.flutter.embedding.android.EnableImpeller\"\n             android:value=\"false\" \n        />\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n        <!-- flutter_foreground_task service -->\n        <service\n            android:name=\"com.pravera.flutter_foreground_task.service.ForegroundService\"\n            android:foregroundServiceType=\"dataSync\"\n            android:exported=\"false\" />\n    </application>\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\"/>\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"/>\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n    <!-- Required to query activities that can process text, see:\n         https://developer.android.com/training/package-visibility?hl=en and\n         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.\n\n         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\"/>\n            <data android:mimeType=\"text/plain\"/>\n        </intent>\n    </queries>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/example/kazumi/MainActivity.kt",
    "content": "package com.example.kazumi\n\nimport android.content.Intent\nimport android.os.Build\nimport android.os.StatFs\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.annotation.NonNull\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n    private val CHANNEL = \"com.predidit.kazumi/intent\"\n    private val STORAGE_CHANNEL = \"com.predidit.kazumi/storage\"\n\n    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->\n            if (call.method == \"openWithMime\") {\n                val url = call.argument<String>(\"url\")\n                val mimeType = call.argument<String>(\"mimeType\")\n                if (url != null && mimeType != null) {\n                    openWithMime(url, mimeType)\n                    result.success(null)\n                } else {\n                    result.error(\"INVALID_ARGUMENT\", \"URL and MIME type required\", null)\n                }\n            } else if (call.method == \"checkIfInMultiWindowMode\") {\n                val isInMultiWindow = checkIfInMultiWindowMode()\n                result.success(isInMultiWindow)\n            } else if (call.method == \"getAndroidSdkVersion\") {\n                val sdkVersion = getAndroidSdkVersion()\n                result.success(sdkVersion)\n            } else {\n                result.notImplemented()\n            }\n        }\n\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, STORAGE_CHANNEL).setMethodCallHandler { call, result ->\n            if (call.method == \"getAvailableStorage\") {\n                val path = call.argument<String>(\"path\") ?: filesDir.absolutePath\n                val availableBytes = getAvailableStorage(path)\n                result.success(availableBytes)\n            } else {\n                result.notImplemented()\n            }\n        }\n    }\n\n    private fun openWithMime(url: String, mimeType: String) {\n        val intent = Intent()\n        intent.action = Intent.ACTION_VIEW\n        intent.setDataAndType(Uri.parse(url), mimeType)\n        startActivity(intent)\n    }\n\n    private fun checkIfInMultiWindowMode(): Boolean {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            this.isInMultiWindowMode \n        } else {\n            false \n        }\n    }\n\n    private fun getAndroidSdkVersion(): Int {\n        return Build.VERSION.SDK_INT\n    }\n\n    private fun getAvailableStorage(path: String): Long {\n        return try {\n            val stat = StatFs(path)\n            stat.availableBlocksLong * stat.blockSizeLong\n        } catch (e: Exception) {\n            -1L\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@color/ic_launcher_background\"/>\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>\n"
  },
  {
    "path": "android/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#ffffff</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <item name=\"android:defaultFocusHighlightEnabled\">false</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:targetApi=\"o_mr1\">shortEdges</item>\n\n        <item name=\"android:defaultFocusHighlightEnabled\">false</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <item name=\"android:defaultFocusHighlightEnabled\">false</item>\n\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:targetApi=\"o_mr1\">shortEdges</item>\n\n        <item name=\"android:defaultFocusHighlightEnabled\">false</item>\n\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-all.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "pluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\").withInputStream { properties.load(it) }\n        def flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n        assert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\n        return flutterSdkPath\n    }\n    settings.ext.flutterSdkPath = flutterSdkPath()\n\n    includeBuild(\"${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id \"dev.flutter.flutter-plugin-loader\" version \"1.0.0\"\n    id \"com.android.application\" version \"8.7.0\" apply false\n    id \"org.jetbrains.kotlin.android\" version \"2.1.0\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "assets/bbcode/BBCode.g4",
    "content": "grammar BBCode;\n\noptions { language=Dart; }\n\ndocument\n    : element* EOF\n    ;\n\nelement\n    : tag\n    | plain\n    | bgm\n    | sticker\n    ;\n\ntag\n    : '[' tagName=STRING ('=' attr=STRING)? ']' content=element* '[/' STRING ']'\n    ;\n\nplain\n    : (STRING | '=' | '/' | '[' | ']' | '(' | ')')+\n    // workaround unless these will break tag reconginze\n    | '[来自Bangumi for android]'\n    | '[来自Bangumi for iOS]'\n    ;\n\nbgm\n    : ('(bgm' | '(BGM') id=STRING ')'\n    ;\n\nsticker\n    : '(=A=)'\n    | '(=w=)'\n    | '(-w=)'\n    | '(S_S)'\n    | '(=v=)'\n    | '(@_@)'\n    | '(=W=)'\n    | '(TAT)'\n    | '(T_T)'\n    | '(=\\'=)'\n    | '(=3=)'\n    | '(= =\\')'\n    | '(=///=)'\n    | '(=.,=)'\n    | '(:P)'\n    | '(LOL)';\n\nSTRING : ~[=[\\]()]+;\n"
  },
  {
    "path": "assets/linux/DEBIAN/postinst",
    "content": "#!/usr/bin/env sh\nln -sf /opt/Kazumi/kazumi /usr/bin/kazumi\nchmod +x /usr/bin/kazumi\nupdate-mime-database /usr/share/mime || true\nupdate-desktop-database /usr/share/applications || true\nexit 0"
  },
  {
    "path": "assets/linux/DEBIAN/postrm",
    "content": "#!/usr/bin/env sh\nrm /usr/bin/kazumi\nupdate-mime-database /usr/share/mime || true\nupdate-desktop-database /usr/share/applications || true\nexit 0"
  },
  {
    "path": "assets/linux/io.github.Predidit.Kazumi.desktop",
    "content": "[Desktop Entry]\nName=Kazumi\nComment=Watch Animes online with danmaku support.\nComment[zh_CN]=一款好用的追番软件\nExec=kazumi\nStartupWMClass=kazumi\nIcon=io.github.Predidit.Kazumi\nTerminal=false\nType=Application\nCategories=AudioVideo;Audio;Video;\n"
  },
  {
    "path": "assets/plugins/7sefun.json",
    "content": "{\n    \"api\": \"4\",\n    \"type\": \"anime\",\n    \"name\": \"7sefun\",\n    \"version\": \"1.2\",\n    \"muliSources\": true,\n    \"useWebview\": true,\n    \"useNativePlayer\": true,\n    \"userAgent\": \"\",\n    \"baseURL\": \"https://www.7sefun.top/\",\n    \"searchURL\": \"https://www.7sefun.top/vodsearch/-------------.html?wd=@keyword\",\n    \"searchList\": \"//div[2]/div[2]/div[2]/div[2]/div\",\n    \"searchName\": \"//div[2]/text()\",\n    \"searchResult\": \"//a\",\n    \"chapterRoads\": \"//div[2]/div[2]/div[2]/div/div[2]/div[1]/div[2]\",\n    \"chapterResult\": \"//a\"\n}"
  },
  {
    "path": "assets/plugins/AGE.json",
    "content": "{\n    \"api\": \"1\",\n    \"type\": \"anime\",\n    \"name\": \"AGE\",\n    \"version\": \"1.5\",\n    \"muliSources\": true,\n    \"useWebview\": true,\n    \"useNativePlayer\": true,\n    \"userAgent\": \"\",\n    \"baseURL\": \"https://www.agedm.io/\",\n    \"searchURL\": \"https://www.agedm.io/search?query=@keyword\",\n    \"searchList\": \"//div[2]/div/section/div/div/div/div\",\n    \"searchName\": \"//div/div[2]/h5/a\",\n    \"searchResult\": \"//div/div[2]/h5/a\",\n    \"chapterRoads\": \"//div[2]/div/section/div/div[2]/div[2]/div[2]/div\",\n    \"chapterResult\": \"//ul/li/a\"\n}"
  },
  {
    "path": "assets/plugins/DM84.json",
    "content": "{\n    \"api\": \"5\",\n    \"type\": \"anime\",\n    \"name\": \"DM84\",\n    \"version\": \"1.4\",\n    \"muliSources\": true,\n    \"useWebview\": true,\n    \"useNativePlayer\": true,\n    \"userAgent\": \"\",\n    \"adBlocker\": true,\n    \"baseURL\": \"https://dmbus.cc/\",\n    \"searchURL\":\"https://dmbus.cc/s----------.html?wd=@keyword\",\n    \"searchList\": \"//div/div[3]/ul/li\",\n    \"searchName\": \"//div/a[2]\",\n    \"searchResult\": \"//div/a[2]\",\n    \"chapterRoads\": \"//div/div[4]/div/ul\",\n    \"chapterResult\": \"//li/a\"\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_AutoDownscalePre_x2.glsl",
    "content": "// This is free and unencumbered software released into the public domain.\n\n// Anyone is free to copy, modify, publish, use, compile, sell, or\n// distribute this software, either in source code form or as a compiled\n// binary, for any purpose, commercial or non-commercial, and by any\n// means.\n\n// In jurisdictions that recognize copyright laws, the author or authors\n// of this software dedicate any and all copyright interest in the\n// software to the public domain. We make this dedication for the benefit\n// of the public at large and to the detriment of our heirs and\n// successors. We intend this dedication to be an overt act of\n// relinquishment in perpetuity of all present and future rights to this\n// software under copyright law.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\n// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\n// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n// OTHER DEALINGS IN THE SOFTWARE.\n\n// For more information, please refer to <https://unlicense.org>\n\n//!DESC Anime4K-v4.0-AutoDownscalePre-x2\n//!HOOK MAIN\n//!BIND HOOKED\n//!BIND NATIVE\n//!WHEN OUTPUT.w NATIVE.w / 2.0 < OUTPUT.h NATIVE.h / 2.0 < * OUTPUT.w NATIVE.w / 1.2 > OUTPUT.h NATIVE.h / 1.2 > * *\n//!WIDTH OUTPUT.w\n//!HEIGHT OUTPUT.h\n\nvec4 hook() {\n\treturn HOOKED_tex(HOOKED_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_AutoDownscalePre_x4.glsl",
    "content": "// This is free and unencumbered software released into the public domain.\n\n// Anyone is free to copy, modify, publish, use, compile, sell, or\n// distribute this software, either in source code form or as a compiled\n// binary, for any purpose, commercial or non-commercial, and by any\n// means.\n\n// In jurisdictions that recognize copyright laws, the author or authors\n// of this software dedicate any and all copyright interest in the\n// software to the public domain. We make this dedication for the benefit\n// of the public at large and to the detriment of our heirs and\n// successors. We intend this dedication to be an overt act of\n// relinquishment in perpetuity of all present and future rights to this\n// software under copyright law.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\n// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\n// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n// OTHER DEALINGS IN THE SOFTWARE.\n\n// For more information, please refer to <https://unlicense.org>\n\n//!DESC Anime4K-v3.2-AutoDownscalePre-x4\n//!HOOK MAIN\n//!BIND HOOKED\n//!BIND NATIVE\n//!WHEN OUTPUT.w NATIVE.w / 4.0 < OUTPUT.h NATIVE.h / 4.0 < * OUTPUT.w NATIVE.w / 2.4 > OUTPUT.h NATIVE.h / 2.4 > * *\n//!WIDTH OUTPUT.w 2 /\n//!HEIGHT OUTPUT.h 2 /\n\nvec4 hook() {\n\treturn HOOKED_tex(HOOKED_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Clamp_Highlights.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v4.0-De-Ring-Compute-Statistics\n//!HOOK MAIN\n//!BIND HOOKED\n//!SAVE STATSMAX\n//!COMPONENTS 1\n\n#define KERNELSIZE 5 //Kernel size, must be an positive odd integer.\n#define KERNELHALFSIZE 2 //Half of the kernel size without remainder. Must be equal to trunc(KERNELSIZE/2).\n\nfloat get_luma(vec4 rgba) {\n\treturn dot(vec4(0.299, 0.587, 0.114, 0.0), rgba);\n}\n\nvec4 hook() {\n\n\tfloat gmax = 0.0;\n\t\n\tfor (int i=0; i<KERNELSIZE; i++) {\n\t\tfloat g = get_luma(MAIN_texOff(vec2(i - KERNELHALFSIZE, 0)));\n\t\t\n\t\tgmax = max(g, gmax);\n\t}\n\t\n\treturn vec4(gmax, 0.0, 0.0, 0.0);\n}\n\n//!DESC Anime4K-v4.0-De-Ring-Compute-Statistics\n//!HOOK MAIN\n//!BIND HOOKED\n//!BIND STATSMAX\n//!SAVE STATSMAX\n//!COMPONENTS 1\n\n#define KERNELSIZE 5 //Kernel size, must be an positive odd integer.\n#define KERNELHALFSIZE 2 //Half of the kernel size without remainder. Must be equal to trunc(KERNELSIZE/2).\n\nvec4 hook() {\n\n\tfloat gmax = 0.0;\n\t\n\tfor (int i=0; i<KERNELSIZE; i++) {\n\t\tfloat g = STATSMAX_texOff(vec2(0, i - KERNELHALFSIZE)).x;\n\t\t\n\t\tgmax = max(g, gmax);\n\t}\n\t\n\treturn vec4(gmax, 0.0, 0.0, 0.0);\n}\n\n//!DESC Anime4K-v4.0-De-Ring-Clamp\n//!HOOK PREKERNEL\n//!BIND HOOKED\n//!BIND STATSMAX\n\nfloat get_luma(vec4 rgba) {\n\treturn dot(vec4(0.299, 0.587, 0.114, 0.0), rgba);\n}\n\nvec4 hook() {\n\n\tfloat current_luma = get_luma(HOOKED_tex(HOOKED_pos));\n\tfloat new_luma = min(current_luma, STATSMAX_tex(HOOKED_pos).x);\n\t\n\t//This trick is only possible if the inverse Y->RGB matrix has 1 for every row... (which is the case for BT.709)\n\t//Otherwise we would need to convert RGB to YUV, modify Y then convert back to RGB.\n    return HOOKED_tex(HOOKED_pos) - (current_luma - new_luma); \n}"
  },
  {
    "path": "assets/shaders/Anime4K_Restore_CNN_M.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.09991986, 0.13782342, -0.031251684, -0.06356843, -0.3437488, 0.05450952, 0.34347802, 0.46335372, 0.08607224, 0.044988394, 0.137179, 0.17976908, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(-0.024212424, -0.09278509, -0.00040907756, 0.34552294, -0.13254678, 0.113105185, 0.005667946, -0.00036919137, -0.06375679, 0.009184115, 0.115518734, -0.115506776, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(-0.14101827, 0.023523493, 0.044094566, -0.019271746, -0.44348842, -0.08818877, -0.4026149, -0.21995795, -0.15880394, -0.013732858, -0.020751135, 0.012719151, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(0.013001821, -0.34503505, 0.39219138, 0.18792126, 0.24760444, -0.016173402, 0.10154511, 0.15453082, -0.058132876, 0.016784398, -0.05808539, -0.11039915, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(0.37024534, 0.041440863, -0.3374568, -0.44994286, 0.19555596, 0.20855539, -0.27974075, -0.5372628, 0.21228147, -0.0295346, -0.56700057, 0.030042822, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(-0.12940632, 0.057526, 0.090682045, -0.06985033, -0.13704006, -0.047685407, 0.44615674, -0.48056605, -0.06166251, -0.01883519, 0.2032237, -0.113287605, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.010856669, -0.35820737, 0.16757219, 0.082619876, -0.03967303, 0.038705572, 0.32652855, -0.012030017, 0.015120559, -0.15314877, 0.23442009, 0.09767922, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(-0.046272673, -0.17752305, 0.082018286, -0.2512824, 0.58619463, -0.060903464, -0.022793597, 0.077803515, -0.17025311, 0.05136993, 0.029383298, -0.15475409, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(-0.11212024, 0.13378005, -0.2027488, 0.08056421, -0.11176219, -0.048429377, -0.08396386, 0.10507829, 0.13326839, 0.0430627, 0.051362377, 0.06482755, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(-0.061233472, 0.39222646, 0.029704979, 0.02586828);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.16410656, -0.40521824, 0.13121907, -0.02314597, 0.105412476, -0.060401272, -0.043063477, -0.13933973, 0.12558138, -0.020861467, 0.030370515, 0.13178016, -0.14220351, 0.20736893, 0.003321564, -0.29241714) * go_0(-1.0, -1.0);\n    result += mat4(0.18517321, 0.29162985, -0.26783395, 0.039760686, 0.025527012, -0.067319244, 0.055004176, 0.048916563, 0.12750523, -0.091435954, 0.13818842, 0.36704224, 0.0839921, 0.10186618, -0.17237376, 0.13282418) * go_0(-1.0, 0.0);\n    result += mat4(-0.1657887, 0.0131325135, -0.17222486, 0.091398895, -0.12756164, -0.08437298, -0.29052997, 0.3269337, 0.15870757, -0.013529402, -0.0581753, 0.11802371, 0.07099966, -0.024063632, 0.31834844, -0.11183859) * go_0(-1.0, 1.0);\n    result += mat4(0.46036887, -0.07654623, 0.22923063, 0.17463821, 0.10555414, -0.117430426, 0.12406777, -0.011399492, 0.028316498, 0.13684341, 0.009664087, 0.2022659, 0.04953974, -0.31342217, -0.6103131, -0.13605757) * go_0(0.0, -1.0);\n    result += mat4(0.03406955, -0.39819366, 0.61176, -0.46809456, -0.029321073, 0.46619493, 0.36700186, 0.02288561, 0.11464085, -0.10931452, -0.09154022, 0.07334147, -0.5609916, 0.31826234, -0.011012659, -0.46719545) * go_0(0.0, 0.0);\n    result += mat4(-0.056855045, 0.27037027, -0.09269696, -0.563572, -0.06816116, -0.22986612, 0.08693167, -0.16246101, 0.09954046, -0.05374176, 0.0071916827, -0.1788692, 0.3825241, -0.1609887, 0.055204768, 0.10213068) * go_0(0.0, 1.0);\n    result += mat4(0.0646626, 0.102358796, -0.45055822, 0.20557903, -0.23337309, 0.12633002, -0.19299199, -0.15085731, -0.13473304, 0.053790465, -0.10061193, -0.13393497, -0.04264752, -0.029740738, -0.07865285, 0.20883279) * go_0(1.0, -1.0);\n    result += mat4(0.010471527, -0.033218473, -0.46157447, 0.004866583, 0.23226471, -0.059343327, -0.1439596, 0.13619648, 0.013839963, 0.15930325, 0.043742355, 0.17467323, 0.33772305, 0.40261495, -0.08351293, 0.18129359) * go_0(1.0, 0.0);\n    result += mat4(-0.12493434, -0.1875134, -0.074943796, -0.0031701606, -0.037142616, 0.1667002, 0.16665547, -0.011248127, 0.0071619414, 0.0034872112, 0.120318964, -0.09625579, 0.14917047, -0.16310586, 0.07231737, 0.30447328) * go_0(1.0, 1.0);\n    result += mat4(0.093798615, 0.17074613, -0.08780678, -0.012520207, 0.118534856, 0.027508778, -0.2778478, -0.19509242, -0.34137097, 0.32000312, -0.22027159, 0.337515, 0.16220862, 0.108993016, 0.14070526, 0.12784284) * go_1(-1.0, -1.0);\n    result += mat4(-0.14325632, -0.1467453, -0.27502358, 0.09370837, 0.11821083, -0.012266484, -0.2100548, 0.4707502, -0.06766648, 0.58165014, -0.2512279, -0.33783755, 0.1318925, -0.04346277, 0.15454485, 0.044500057) * go_1(-1.0, 0.0);\n    result += mat4(-0.05683207, 0.0051946463, -0.108000524, 0.10133204, -0.50763863, 0.007308442, 0.8542404, 0.28387356, 0.022709515, 0.294523, -0.3822472, 0.66166407, 0.01404485, 0.031282708, -0.26756814, -0.123147786) * go_1(-1.0, 1.0);\n    result += mat4(-0.36455178, 0.3470555, -0.045303088, -0.03170764, -0.15802494, -0.0019141496, -0.25939587, -0.23875342, 0.130428, 0.03954273, -0.17985536, 0.105145946, 0.15804817, 0.12551713, 0.28371975, -0.085748516) * go_1(0.0, -1.0);\n    result += mat4(0.0060625463, 0.2443924, -0.017692259, -0.20214005, -0.09584515, -0.012805372, -0.13942227, 0.16143198, 0.12942013, 0.41785547, 0.046071563, 0.7030026, 0.10499644, -0.20566013, -0.031321276, 0.27830327) * go_1(0.0, 0.0);\n    result += mat4(-0.081274964, -0.14562319, 0.27200526, -0.20491314, 0.012910989, 0.024201397, 0.04816258, 0.21297328, -0.22015952, -0.44160756, -0.056035373, 0.33824417, -0.31645304, 0.15469243, 0.053187452, -0.20989445) * go_1(0.0, 1.0);\n    result += mat4(-0.046550367, 0.033185404, 0.33337244, 0.12853645, 0.23520172, -0.05909214, 0.0861368, 0.10706329, -0.07058717, -0.11759937, -0.18594047, 0.080006264, -0.055425353, -0.12506317, 0.15729053, -0.0915004) * go_1(1.0, -1.0);\n    result += mat4(0.042516407, 0.14844789, 0.16533111, 0.13502933, -0.0655417, -0.057256397, 0.076713726, -0.23448966, 0.12855926, 0.014219275, 0.051761385, 0.053433083, -0.2446715, -0.4008074, 0.19603717, -0.1796951) * go_1(1.0, 0.0);\n    result += mat4(0.14777803, 0.15524907, 0.043158617, -0.06996876, 0.19210646, -0.2144364, -0.47020787, -0.4207906, -0.18074386, -0.2163903, 0.0030754965, 0.36799973, -0.3837698, -0.0022661497, -0.37276733, -0.28934997) * go_1(1.0, 1.0);\n    result += vec4(-0.018297346, -0.080951825, -0.062163066, -0.08050014);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.31543177, 0.23095237, -0.06692611, -0.5867763, 0.003622504, 0.17948842, -0.14627707, 0.1745016, -0.052964583, -0.15551159, 0.05644786, -0.012665164, 0.13107763, 0.11369179, -0.09452995, -0.11973403) * go_0(-1.0, -1.0);\n    result += mat4(-0.2694661, -0.115382135, 0.3073268, -0.067228466, -0.25511482, -0.13922207, 0.36758214, -0.18821828, -0.022617863, 0.20333402, -0.11125889, 0.3552245, -0.013346653, -0.099095374, -0.25100616, 0.35521755) * go_0(-1.0, 0.0);\n    result += mat4(0.011012409, -0.13675085, 0.25642, -0.34851208, -0.23184675, 0.18012202, 0.57654136, 0.103173524, -0.16461405, 0.038177088, 0.1234096, 0.013202029, -0.19033363, 0.07469178, -0.017948546, 0.15287702) * go_0(-1.0, 1.0);\n    result += mat4(-0.05340533, 0.23797482, 0.20351392, -0.05333351, -0.12181174, -0.23363493, -0.20696607, 0.109941036, -0.11519453, 0.13842066, -0.10687832, 0.29040006, 0.022218632, 0.031238724, 0.2685182, 0.15300068) * go_0(0.0, -1.0);\n    result += mat4(0.22985318, -0.3103802, -0.22916415, 0.25238806, -0.11690287, -0.1947488, 0.118020535, 0.07814263, -0.06335474, -0.007870727, 0.076106325, 0.094677486, -0.16776285, -0.006570437, -0.29589584, 0.41413507) * go_0(0.0, 0.0);\n    result += mat4(0.43607962, -0.36456433, -0.123776875, -0.16634953, -0.091190875, 0.13035081, 0.28627968, 0.27249968, 0.12356344, -0.008616177, 0.09599816, -0.006144557, -0.23490307, 0.3013123, 0.14153156, 0.21837278) * go_0(0.0, 1.0);\n    result += mat4(0.060364585, 0.37860224, 0.039182413, -0.22805426, -0.089910224, -0.06817697, -0.2684275, -0.12528503, 0.036934495, -0.07826616, 0.06559976, -0.08253646, 0.13489649, 0.06237663, 0.126376, 0.21194184) * go_0(1.0, -1.0);\n    result += mat4(-0.12534817, 0.21225189, -0.27818045, -0.3070443, -0.006957577, -0.025105853, 0.12100924, -0.06916452, 0.23081483, 0.1802756, -0.18995638, 0.16603014, -0.2904096, -0.25292823, -0.21834068, 0.13719653) * go_0(1.0, 0.0);\n    result += mat4(0.017209655, 0.10757137, 0.21414296, -0.30885983, 0.10467716, -0.2184891, 0.100061476, -0.1527528, 0.2100472, -0.25768545, -0.22329919, -0.29153427, -0.06983842, -0.103854865, -0.051384352, 0.14629121) * go_0(1.0, 1.0);\n    result += mat4(0.0059623295, -0.26060802, 0.32115817, 0.021025505, 0.09783085, -0.15865178, 0.1473021, -0.24977303, -0.033508282, 0.17480391, -0.091310136, 0.09870876, 0.10504043, -0.06105686, 0.013493489, -0.11278855) * go_1(-1.0, -1.0);\n    result += mat4(0.14875248, -0.14859414, 0.19377062, -0.17456068, 0.101288855, -0.1113682, -0.48944646, 0.1018565, -0.037392337, 0.08539691, 0.1751306, -0.15428723, -0.059375558, 0.027663672, 0.051804014, -0.049813222) * go_1(-1.0, 0.0);\n    result += mat4(0.118846565, -0.19869871, -0.037388258, 0.08456728, -0.11662527, -0.43818352, -0.093285345, 0.038507205, -0.051991668, 0.21008292, 0.10792365, 0.2020924, 0.057021596, 0.09460527, 0.0016551288, -0.0015957063) * go_1(-1.0, 1.0);\n    result += mat4(0.11062174, -0.2639232, -0.060295466, -0.3217331, -0.050545212, 0.30989558, 0.30906132, 0.030323273, 0.028986752, 0.037429404, 0.20855664, -0.19848943, 0.034687653, -0.09599135, -0.06250494, -0.13215867) * go_1(0.0, -1.0);\n    result += mat4(-0.010391146, 0.07657845, 0.44491258, 0.0435906, 0.0075931503, 0.42632654, 0.47022533, 0.34737435, -0.15452717, -0.14613411, -0.45231065, 0.12094409, 0.0067911847, 0.057501152, 0.09876979, 0.044946447) * go_1(0.0, 0.0);\n    result += mat4(-0.15607435, 0.2293058, -0.09520331, 0.012836732, -0.15282455, 0.26437718, -0.1685477, -0.13211122, -0.055801593, -0.016778728, -0.34478986, -0.23228309, 0.12300962, -0.13235827, -0.13987203, -0.16550972) * go_1(0.0, 1.0);\n    result += mat4(0.13161735, -0.09039346, -0.033475474, -0.23686698, 0.1514885, 0.20977421, 0.031431954, -0.0049226107, 0.090661936, 0.15288061, -0.03316583, 0.09646573, -0.32651708, 0.18825398, -0.15777239, 0.17572704) * go_1(1.0, -1.0);\n    result += mat4(0.112157226, -0.08712878, 0.23453182, 0.1043877, -0.14686783, 0.28682423, -0.086443506, 0.059457052, -0.31530112, -0.2700583, -0.06028952, -0.070416875, 0.18053482, 0.16653341, 0.25215197, 0.061915852) * go_1(1.0, 0.0);\n    result += mat4(-0.20122242, 0.076313145, -0.0988483, 0.094337784, -0.35436687, 0.3762327, -0.07809558, 0.3055848, 0.10425242, -0.17087407, 0.030301496, -0.13911743, 0.01630275, 0.24247427, -0.006474477, 0.03842641) * go_1(1.0, 1.0);\n    result += vec4(-0.008952847, -0.0058945753, -0.08097229, 0.020968592);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!SAVE conv2d_3_tf\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.2237721, -0.0064096362, -0.31808427, 0.73477733, 0.015353088, 0.23983319, 0.14967978, -0.34920225, -0.07456269, 0.093151815, -0.14331086, -0.24586205, -0.14183366, 0.06401045, -0.22044073, 0.29932275) * go_0(-1.0, -1.0);\n    result += mat4(-0.07968509, -0.3349146, 0.16529128, 0.08443499, 0.4095855, -0.17120704, 0.17425705, 0.15298946, 0.2981273, 0.2212369, 0.10392389, -0.28775454, -0.065247655, -0.15255849, 0.13094437, 0.18685219) * go_0(-1.0, 0.0);\n    result += mat4(0.015706737, -0.17755036, 0.2622526, 0.112057306, -0.15876788, -0.38466996, -0.33700845, -0.031711742, -0.023320962, -0.3145249, -0.21223734, -0.1314596, -0.1888095, -0.046370104, 0.09000896, -0.0046378844) * go_0(-1.0, 1.0);\n    result += mat4(-0.31127506, 0.31304324, -0.03965752, 0.03649018, -0.029851055, 0.05801377, 0.00040150844, -0.04422069, 0.18019931, 0.14415511, -0.09845236, 0.21895434, -0.013932474, -0.046454947, -0.3403935, -0.006705289) * go_0(0.0, -1.0);\n    result += mat4(-0.34878647, -0.5129283, 0.060250953, -0.16354133, 0.20644619, 0.08732273, -0.24118888, 0.24455065, 0.24449423, 0.44103387, 0.22455928, 0.25738943, -0.26914698, -0.21309987, 0.08386486, 0.021484816) * go_0(0.0, 0.0);\n    result += mat4(-0.057454903, -0.4121922, 0.022661546, 0.37178272, 0.03331408, 0.05044008, 0.04324371, 0.20727943, 0.2432641, 0.076906696, -0.20858039, 0.012439015, -0.19335061, 0.09217451, 0.1968369, -0.19435833) * go_0(0.0, 1.0);\n    result += mat4(-0.16960496, 0.24616167, 0.37977478, 0.14324574, -0.011531225, -0.11312143, -0.18141079, -0.23843932, 0.0086012175, -0.3564491, -0.12639481, 0.009799298, -0.29120612, 0.23756824, 0.18035695, -0.087133996) * go_0(1.0, -1.0);\n    result += mat4(-0.10081239, 0.29191494, 0.10434693, 0.08970636, 0.008997759, 0.104756236, 0.039641086, 0.02323888, -0.11627765, 0.023693223, -0.30801758, -0.120208986, 0.05086147, 0.18498175, 0.15595439, -0.09877306) * go_0(1.0, 0.0);\n    result += mat4(0.101321675, -0.2929976, 0.38810417, 0.5605376, -0.04073937, 0.030110704, -0.18147062, -0.09833952, 0.01927733, 0.15335669, -0.15384074, -0.110595055, -0.054297395, -0.077522054, 0.07918369, -0.068480626) * go_0(1.0, 1.0);\n    result += mat4(0.23263514, -0.11719232, 0.2903209, -0.007503795, -0.020222448, -0.17790157, -0.15600762, -0.08741775, 0.12529704, 0.25548857, -0.04585447, -0.10255033, 0.18350503, -0.29593533, 0.0868933, 0.027004737) * go_1(-1.0, -1.0);\n    result += mat4(-0.14958654, -0.006238835, -0.2928948, 0.1988557, -0.17057803, 0.12524141, 0.13978264, -0.019280292, 0.05967142, -0.07790818, -0.5893818, -0.022845713, -0.08596779, 0.07875358, -0.03316667, -0.4369282) * go_1(-1.0, 0.0);\n    result += mat4(0.19195688, -0.060883682, -0.25897828, 0.07063324, 0.090833396, 0.003422883, 0.109534174, 0.031180874, -0.05017118, 0.022862168, -0.270113, -0.057831235, 0.53920543, -0.10252776, -0.091807485, 0.004294343) * go_1(-1.0, 1.0);\n    result += mat4(-0.18494242, -0.119284816, 0.3821897, 0.07777979, 0.15568028, -0.2854859, -0.22441281, -0.049155876, -0.15292497, 0.21895619, -0.095677756, 0.15210424, 0.001643022, -0.026176987, 0.048463076, -0.4824009) * go_1(0.0, -1.0);\n    result += mat4(0.007215129, 0.17074333, 0.053930074, -0.027014816, -0.17180431, -0.15163863, -0.0012122132, -0.18934256, -0.08294297, -0.24580221, -0.46552867, -0.27923223, 0.4092668, 0.06288688, -0.1602188, -0.0030876845) * go_1(0.0, 0.0);\n    result += mat4(0.111870885, 0.03317145, 0.14155298, 0.20328505, -0.05104131, 0.13979794, 0.018966835, -0.07238511, 0.05493792, -0.14975783, -0.10293237, -0.21985306, 0.49054706, 0.18288186, -0.26925826, 0.35845932) * go_1(0.0, 1.0);\n    result += mat4(0.3747799, -0.096748486, -0.17139742, 0.25289854, -0.17421168, -0.018461818, 0.09747162, 0.01660535, -0.20580359, 0.56189656, 0.17151354, -0.26347768, 0.28350568, -0.21486014, -0.44330928, -0.008981037) * go_1(1.0, -1.0);\n    result += mat4(0.10169985, -0.18244018, 0.04760736, 0.41017643, -0.09468786, -0.024218475, 0.103733875, -0.22540338, 0.10630112, 0.3677178, -0.104170956, 0.057317447, 0.21764882, 0.0789158, -0.22041337, 0.15065216) * go_1(1.0, 0.0);\n    result += mat4(0.11633995, -0.008195114, -0.14501533, 0.07168025, 0.058413275, 0.055995367, 0.09362145, -0.13827963, 0.13760869, 0.040319785, 0.038895044, 0.2675253, -0.087339684, 0.1412073, -0.17166458, -0.2312994) * go_1(1.0, 1.0);\n    result += vec4(-0.059377354, -0.02055341, 0.07234869, -0.015452986);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!SAVE conv2d_4_tf\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.29012984, -0.13150147, 0.31015614, 0.05992291, -0.050289866, 0.14845313, -0.09608898, 0.27913308, 0.060307387, -0.04160452, 0.035932682, -0.08137563, -0.07999419, 0.11818284, -0.27512288, 0.21948813) * go_0(-1.0, -1.0);\n    result += mat4(0.12916058, -0.21759962, -0.33868533, 0.021636661, 0.053470243, 0.1412425, 0.043395396, -0.26751056, -0.01689101, -0.2623835, 0.010809152, 0.062962815, -0.20692012, -0.1677863, -0.23313859, -0.17402615) * go_0(-1.0, 0.0);\n    result += mat4(-0.08204112, -0.23672083, -0.0064437394, -0.13200696, -0.056692924, -0.02708657, 0.12536962, 0.004428919, 0.14137582, 0.15404348, -0.105753876, 0.047957454, 0.15734316, 0.16562423, -0.010160829, -0.06602983) * go_0(-1.0, 1.0);\n    result += mat4(0.025653997, -0.10877775, -0.31258908, 0.18841636, -0.36005193, 0.1816357, -0.34537643, -0.0741087, 0.4663994, 0.0065186517, 0.08109033, 0.2976773, -0.35774228, -0.041366056, -0.37852773, 0.050565656) * go_0(0.0, -1.0);\n    result += mat4(0.04392313, 0.11316681, -0.14421389, 0.17985669, -0.1651274, -0.5656209, -0.124100484, 0.42774054, -0.1153939, 0.16829851, 0.2025612, 0.054007456, -0.06868256, -0.56935954, -0.12227961, 0.17688861) * go_0(0.0, 0.0);\n    result += mat4(0.34041, 0.499, 0.15234196, 0.21353458, -0.2732667, -0.049950935, 0.03550811, -0.21051687, 0.2609023, 0.016438454, -0.29874632, 0.37994128, 0.049288407, -0.31126305, 0.029235512, -0.012256015) * go_0(0.0, 1.0);\n    result += mat4(-0.0046853204, 0.15391374, -0.040689662, 0.20186873, -0.08137621, 0.35905558, 0.23733845, 0.21794793, -0.066420384, 0.029600656, -0.31421044, -0.050773863, -0.06260773, 0.04634221, -0.10948491, -0.045498934) * go_0(1.0, -1.0);\n    result += mat4(-0.082953, -0.025837064, -0.09928303, -0.14300232, 0.275064, 0.07793617, 0.22240888, 0.06637834, -0.4382666, -0.2932182, -0.27243167, -0.14221182, 0.5695728, 0.20719238, 0.5575927, 0.40816882) * go_0(1.0, 0.0);\n    result += mat4(-0.18510929, -0.15052167, 0.25277212, 0.06804461, 0.016387, 0.20310035, 0.2903229, -0.0615877, -0.28987274, -0.11942605, 0.013498961, 0.3184152, 0.29543474, -0.042830903, -0.018111207, -0.13263674) * go_0(1.0, 1.0);\n    result += mat4(0.25749087, 0.0053866603, -0.09391162, -0.06129529, -0.094091184, -0.07419633, 0.0013858611, 0.012000353, -0.062903, -0.0204224, -0.12113313, 0.017942557, -0.073379934, 0.052201986, 0.35864577, 0.023564404) * go_1(-1.0, -1.0);\n    result += mat4(0.100115694, 0.19451359, 0.23252094, 0.19506809, -0.12470779, 0.0027281935, -0.17488572, -0.018721964, -0.15159339, 0.18457152, 0.057712987, -0.08191495, 0.19735703, 0.07326743, -0.28563106, 0.01642815) * go_1(-1.0, 0.0);\n    result += mat4(0.068062514, 0.28356665, 0.07377898, 0.42776972, 0.28725025, -0.13045293, -0.17525704, -0.05885591, -0.16676305, -0.2555945, -0.10078422, -0.053032875, 0.084470876, 0.06460686, 0.13824362, -0.05231353) * go_1(-1.0, 1.0);\n    result += mat4(0.22637829, -0.028969254, 0.1968254, -0.13331996, 0.038017053, -0.008854481, -0.2031639, 0.09237089, -0.3821112, 0.1108527, -0.11029933, -0.24542028, 0.22416145, -0.031492114, -0.19144306, -0.0996271) * go_1(0.0, -1.0);\n    result += mat4(0.10776744, 0.16363445, 0.14656505, -0.3737814, -0.06642015, 0.5616549, -0.008412252, -0.37266847, 0.12506576, -0.15329036, 0.037538245, -0.10810259, 0.01706349, 0.1813702, 0.035651788, -0.012786579) * go_1(0.0, 0.0);\n    result += mat4(-0.4023338, -0.2098614, -0.18285121, -0.02727653, 0.26107362, 0.041306913, -0.036515504, -0.045217298, -0.39958602, -0.21229339, -0.021053292, -0.13427502, 0.36178818, 0.20934913, 0.1500852, 0.2634554) * go_1(0.0, 1.0);\n    result += mat4(0.07794611, -0.25937587, -0.06822529, -0.056336135, 0.094220124, 0.21588847, -0.0455218, -0.10968329, -0.08068449, -0.31366697, 0.07799637, 0.24252681, 0.23963861, 0.13715535, 0.010329345, 0.09094301) * go_1(1.0, -1.0);\n    result += mat4(-0.20975718, -0.12550138, 0.14453574, -0.0020878632, -0.07153068, 0.3249998, -0.056577377, 0.18166828, 0.37204072, 0.17018336, 0.3752895, 0.32178587, 0.2571982, -0.27258632, -0.25971004, -0.40536007) * go_1(1.0, 0.0);\n    result += mat4(-0.3243907, -0.06300621, -0.09398436, -0.19549188, 0.14906861, 0.061537784, -0.055284478, 0.11281728, 0.12964857, 0.09979093, -0.1810159, -0.4104283, 0.05807971, -0.056371246, 0.08072554, 0.18479007) * go_1(1.0, 1.0);\n    result += vec4(-0.048888464, -0.0561434, 0.030690912, -0.030496685);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!SAVE conv2d_5_tf\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.15332128, 0.027258258, 0.14900503, -0.15982795, 0.17021236, -0.51046044, -0.15287271, -0.058167327, 0.51826185, -0.34817994, 0.004513167, 0.05395769, 0.1990321, -0.049979225, 0.11391989, -0.16062729) * go_0(-1.0, -1.0);\n    result += mat4(0.033682905, 0.019728886, 0.19931756, 0.17381927, 0.2585768, -0.2124572, -0.014632459, 0.39779893, -0.1146207, -0.2396625, 0.08960277, 0.38345298, 0.25497693, 0.11692859, -0.14207517, 0.12667973) * go_0(-1.0, 0.0);\n    result += mat4(-0.14911255, 0.08910706, 0.16136818, 0.03914566, 0.24204038, -0.03607149, -0.4571109, 0.10802461, -0.0021356856, 0.00885878, 0.22297303, 0.2367231, 0.045177583, 0.11120606, -0.009971904, -0.059262395) * go_0(-1.0, 1.0);\n    result += mat4(0.24565999, -0.2261384, 0.47373205, 0.024613412, -0.10923052, 0.039027315, -0.42707404, -0.3783373, 0.3544573, -0.5468578, -0.27599156, -0.09455918, 0.18760219, -0.19082001, 0.030565469, 0.20589156) * go_0(0.0, -1.0);\n    result += mat4(0.1973198, -0.03433863, 0.059960485, 0.045642868, 0.1819595, -0.14460869, 0.1286175, 0.2067575, -0.042632047, -0.11842967, -0.11224446, -0.18764776, -0.19563004, 0.027425969, 0.24056377, 0.5949649) * go_0(0.0, 0.0);\n    result += mat4(0.055027682, 0.16331595, -0.2608588, 0.12545955, 0.4588985, 0.03642909, 0.22187738, 0.45190734, -0.001210133, -0.057651415, -0.061199043, 0.11935476, -0.049561135, 0.27509886, 0.13778673, -0.124914035) * go_0(0.0, 1.0);\n    result += mat4(-0.02257459, 0.27705106, 0.044165276, -0.26521233, 0.05982374, -0.2824302, 0.3171142, 0.08430561, -0.10155528, 0.16182268, -0.09183147, -0.19447176, 0.3295707, -0.50616395, -0.036964044, 0.23166709) * go_0(1.0, -1.0);\n    result += mat4(-0.0232342, 0.07299799, -0.18038079, -0.13672702, -0.108305976, 0.15024792, -0.19531927, 0.0870979, -0.26488534, 0.19481428, 0.10737945, -0.14573483, -0.33094683, 0.24155116, -0.09850332, 0.2797003) * go_0(1.0, 0.0);\n    result += mat4(-0.24089853, 0.19506595, 0.4799156, -0.058313113, 0.36212957, -0.44844806, 0.23864488, 0.15477742, -0.07795971, -0.0033861927, -0.11216164, 0.033454563, -0.25893036, 0.23793478, -0.15769425, -0.00033481256) * go_0(1.0, 1.0);\n    result += mat4(0.05772507, -0.1640253, -0.13499664, -0.20460358, -0.024399966, 0.14966168, -0.090857334, -0.039677754, 0.00036956606, -0.24236615, -0.053542696, -0.0049544116, 0.026651502, 0.39019194, -0.2742246, -0.061242323) * go_1(-1.0, -1.0);\n    result += mat4(-0.016323274, -0.036179908, 0.029965919, 0.11151491, -0.00016685206, -0.29573023, 0.17996423, -0.20145437, 0.1324275, -0.18442132, -0.24618152, 0.061780427, -0.02770517, 0.28452995, 0.39804098, -0.1174389) * go_1(-1.0, 0.0);\n    result += mat4(-0.025068847, -0.053328387, -0.27053785, 0.26866457, -0.09866204, 0.057677213, 0.01850112, -0.18014707, -0.13319959, -0.14411181, -0.26355243, -0.022209354, -0.05062645, -0.036771543, 0.13294417, -0.18458557) * go_1(-1.0, 1.0);\n    result += mat4(-0.046194963, 0.038230438, -0.08993043, -0.07236354, 0.11031123, -0.16504908, -0.09517036, -0.16459833, -0.5279925, 0.12686682, -0.05726125, 0.055361677, 0.31593755, 0.027328093, 0.001839602, 0.30581662) * go_1(0.0, -1.0);\n    result += mat4(0.08608678, 0.03168437, 0.007713377, -0.26140293, -0.1268983, 0.13395861, -0.069848835, -0.24080403, 0.018839337, -0.049821075, -0.21461345, -0.14168301, -0.0872339, 0.47096667, 0.022512507, 0.14860632) * go_1(0.0, 0.0);\n    result += mat4(0.06293673, 0.22462969, 0.045494985, 0.021673543, 0.18227446, -0.2956555, 0.08010543, -0.01919729, -0.012190269, 0.241983, -0.046537094, -0.40094566, -0.3853647, 0.1081711, -0.16926058, 0.16138376) * go_1(0.0, 1.0);\n    result += mat4(-0.14854589, -0.17625804, -0.10849075, 0.221543, 0.099971965, 0.13901573, 0.29464146, 0.020068526, 0.054358527, -0.10351705, -0.0062914286, 0.24127026, -0.16914125, 0.12729423, -0.18377453, -0.6452375) * go_1(1.0, -1.0);\n    result += mat4(0.12603393, -0.10986093, 0.2314103, 0.16915044, -0.13619255, -0.09349073, 0.20594226, -0.34507084, 0.19077192, 0.052500796, 0.07185645, 0.029082738, -0.015576321, 0.08254907, -0.5501743, -0.38495848) * go_1(1.0, 0.0);\n    result += mat4(0.09300796, -0.079218306, 0.46825135, -0.08735625, 0.06321122, 0.16234867, 0.042932414, -0.013057422, 0.09697148, 0.23457524, 0.19417483, -0.16804664, 0.18379296, 0.17770062, -0.050235, -0.059676602) * go_1(1.0, 1.0);\n    result += vec4(0.011169491, 0.032399546, 0.138099, 0.023857072);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!SAVE conv2d_6_tf\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.22753362, -0.08612073, 0.33140692, 0.08699529, -0.18788953, -0.056579117, -0.12905197, -0.06694621, 0.054559365, 0.15031597, -0.13430363, 0.021646025, 0.14884405, -0.0694291, 0.26149413, 0.11270503) * go_0(-1.0, -1.0);\n    result += mat4(0.17876762, -0.09637848, 0.11285323, 0.2004893, 0.1317187, -0.036162686, 0.17958368, -0.069625, 0.28760737, -0.12505141, 0.12760694, 0.047717955, -0.16811855, -0.16340709, 0.13278298, -0.08403954) * go_0(-1.0, 0.0);\n    result += mat4(-0.21917523, 0.079711854, -0.28642535, 0.2822416, 0.03001489, -0.014772918, -0.3487396, 0.10597145, -0.013841082, 0.17034237, 0.10810282, -0.08089695, -0.22184245, -0.59067357, 0.44113398, 0.13045649) * go_0(-1.0, 1.0);\n    result += mat4(-0.29906932, 0.013923749, 0.2031124, -0.11846688, -0.13953634, 0.08003455, -0.10164494, -0.21218559, 0.10563715, 0.31033117, -0.075903505, 0.047310907, -0.37824214, -0.14506383, 0.11866701, -0.21384487) * go_0(0.0, -1.0);\n    result += mat4(-0.1353849, 0.19258606, 0.063908584, -0.2043788, 0.27244982, 0.1665306, -0.29357895, -0.22441709, 0.18514316, -0.17840464, 0.20986097, 0.14351055, -0.057732623, 0.42166704, -0.23182064, -0.4957248) * go_0(0.0, 0.0);\n    result += mat4(-0.34830126, 0.109066755, -0.28285867, -0.048280068, -0.12290918, 0.04291651, -0.047484186, -0.03702595, 0.23047262, 0.09398974, 0.022467108, 0.08271034, 0.3066665, -0.54077, 0.057771873, 0.23194093) * go_0(0.0, 1.0);\n    result += mat4(-0.17731948, -0.3175927, 0.1452728, 0.09396786, -0.16433562, -0.01833653, -0.22345604, -0.04161193, -0.14827462, 0.18544114, -0.15544125, -0.06179007, 0.16989979, -0.20985202, 0.16391534, -0.09447268) * go_0(1.0, -1.0);\n    result += mat4(-0.053878862, -0.21034616, 0.023831524, 0.19772215, 0.31647214, 0.0126534775, -0.19130844, -0.049282108, -0.21446131, 0.067189045, 0.09117449, -0.25548774, 0.12109098, 0.22009392, -0.3924665, -0.13340388) * go_0(1.0, 0.0);\n    result += mat4(-0.16096684, -0.18495405, 0.10410178, 0.0015673033, -0.00183498, -0.044303037, -0.062745355, -0.090802394, 0.043269135, 0.06924481, -0.21367405, -0.14619029, 0.11555763, -0.20292862, 0.5799557, 0.14739846) * go_0(1.0, 1.0);\n    result += mat4(-0.21030277, -0.09578802, 0.013482288, -0.21484336, 0.12995781, 0.40431052, -0.3347856, -0.18183486, 0.15550353, -0.04402301, 0.4603779, 0.14874357, -0.07694621, -0.053523075, -0.19607326, -0.10850742) * go_1(-1.0, -1.0);\n    result += mat4(-0.2347211, 0.2697403, -0.0634794, -0.17925987, 0.17231455, 0.24999185, -0.5208536, -0.10491828, -0.233575, 0.52950364, 0.0038063182, -0.1380038, 0.022935199, 0.19369157, 0.14586553, 0.1938704) * go_1(-1.0, 0.0);\n    result += mat4(-0.10245223, 0.34150192, 0.25862157, -0.20165509, 0.5597771, 0.114510864, -0.122526556, -0.04010975, 0.1704679, -0.23335956, -0.16771887, -0.03783455, -0.056995615, 0.24153493, -0.08082429, -0.24210933) * go_1(-1.0, 1.0);\n    result += mat4(-0.103466526, 0.15278348, -0.30526164, -0.080755696, 0.103505425, 0.15862796, 0.14696524, -0.008358076, -0.09180311, -0.12505089, 0.28052542, -0.13551563, 0.07528779, -0.09636086, -0.10369617, 0.23656134) * go_1(0.0, -1.0);\n    result += mat4(-0.25752836, 0.099439755, -0.30716348, 0.035077725, 0.023509016, 0.23106368, 0.05277125, 0.34910464, 0.088015385, 0.26995596, 0.1390645, -0.40671825, 0.18096298, -0.100688554, 0.5492049, 0.2482101) * go_1(0.0, 0.0);\n    result += mat4(0.41411775, -0.107200556, -0.13813478, 0.13768874, 0.27137747, 0.06313619, -0.08522967, 0.03218302, -0.03166121, -0.3415683, -0.52242, -0.1741813, -0.36956537, 0.179129, -0.09742935, -0.11696616) * go_1(0.0, 1.0);\n    result += mat4(-0.07975504, 0.17964838, 0.37122533, 0.16064765, 0.14309953, 0.29473078, 0.0926391, -0.22333665, 0.34612748, -0.3387473, 0.0077308523, -0.07239449, 0.18522519, -0.21297298, 0.11493978, 0.16117814) * go_1(1.0, -1.0);\n    result += mat4(-0.17402779, 0.10023144, 0.11712206, 0.031971734, 0.18713303, 0.08736295, 0.013007052, -0.06943139, -0.20102951, -0.010721135, -0.2562522, 0.34877458, -0.13732676, -0.40258047, 0.25824392, 0.15720639) * go_1(1.0, 0.0);\n    result += mat4(0.044494305, 0.3296108, 0.0017603852, 0.09362289, 0.38839245, 0.40015858, -0.13395199, -0.044521853, -0.56266373, 0.251378, 0.5005789, -0.13106057, -0.18491416, -0.046887, 0.067797676, -0.14694957) * go_1(1.0, 1.0);\n    result += vec4(0.013687534, -0.08185164, -0.04755438, 0.290178);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-3x1x1x56\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_1_tf\n//!BIND conv2d_2_tf\n//!BIND conv2d_3_tf\n//!BIND conv2d_4_tf\n//!BIND conv2d_5_tf\n//!BIND conv2d_6_tf\n//!SAVE MAIN\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n#define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.08837163, -0.065234736, -0.034704313, 0.0, 0.021405501, 0.013663729, 0.019249594, 0.0, 0.05328863, 0.03580334, 0.046457592, 0.0, -0.12216048, 0.022547891, 0.016400825, 0.0) * g_0;\n    result += mat4(0.061996464, 0.05631466, 0.06808407, 0.0, -0.005013109, -0.0044589997, -0.032367796, 0.0, 0.016481603, 0.13721058, 0.14924648, 0.0, 0.020035887, -0.07250003, -0.08034037, 0.0) * g_1;\n    result += mat4(0.24078514, 0.081361525, 0.053420708, 0.0, -0.009353794, -0.051077116, -0.058007747, 0.0, -0.14071098, 0.01035966, 0.005308949, 0.0, -0.1489842, -0.06711817, -0.05552926, 0.0) * g_2;\n    result += mat4(-0.13002375, 0.012733757, 0.017821986, 0.0, 0.17767483, 0.20204604, 0.1751779, 0.0, 0.12804912, 0.07381453, 0.05655911, 0.0, 0.17044514, 0.07301451, 0.06523978, 0.0) * g_3;\n    result += mat4(-0.1170986, -0.05130371, -0.027939914, 0.0, -0.16645707, -0.121526904, -0.09471366, 0.0, -0.04143118, 0.026693767, 0.034615446, 0.0, -0.084318705, -0.064990036, -0.054324172, 0.0) * g_4;\n    result += mat4(0.12094524, 0.09518409, 0.07387219, 0.0, 0.062216382, 0.053228356, 0.031372335, 0.0, 0.072797105, 0.026258165, 0.009804673, 0.0, 0.120719045, 0.073281154, 0.056623302, 0.0) * g_5;\n    result += mat4(-0.11141495, -0.11566289, -0.10398725, 0.0, -0.0651895, -0.06820691, -0.054204144, 0.0, -0.032746475, -0.008849683, -0.007610222, 0.0, -0.024655705, -0.048778858, -0.041144755, 0.0) * g_6;\n    result += mat4(0.058090195, 0.07538767, 0.059722915, 0.0, 0.044788487, 0.04212742, 0.027502589, 0.0, 0.04892866, 0.015416752, 0.008312418, 0.0, -0.011864114, -0.0074752793, -0.0060824654, 0.0) * g_7;\n    result += mat4(0.043446552, 0.061971307, 0.05758086, 0.0, -0.06379154, -0.053758245, -0.047204215, 0.0, 0.016307736, 0.03423424, 0.030179083, 0.0, 0.041445345, 0.03843772, 0.033059113, 0.0) * g_8;\n    result += mat4(-0.003803544, 0.0008906116, -0.00059585314, 0.0, 0.102071285, 0.11485224, 0.10007254, 0.0, -0.074306004, -0.08803551, -0.07972321, 0.0, -0.030704215, -0.021514274, -0.009049376, 0.0) * g_9;\n    result += mat4(0.0066058086, 0.0011408008, 0.0016199006, 0.0, -0.03916473, -0.042929266, -0.04018418, 0.0, -0.03153446, -0.039413508, -0.034767237, 0.0, 0.113516055, 0.12577052, 0.113335624, 0.0) * g_10;\n    result += mat4(0.02655948, 0.041905303, 0.03861737, 0.0, 0.048471425, 0.049788587, 0.050447535, 0.0, 0.12092813, 0.13564217, 0.12613249, 0.0, -0.0023508538, 0.0012828974, 0.0028730957, 0.0) * g_11;\n    result += mat4(0.0084758485, 0.008800083, 0.008206044, 0.0, -0.056123603, -0.06610845, -0.060320783, 0.0, -0.081793964, -0.101638645, -0.096699014, 0.0, -0.04402356, -0.04177539, -0.03829645, 0.0) * g_12;\n    result += mat4(0.10676299, 0.118409514, 0.10618478, 0.0, -0.05880252, -0.06488367, -0.06432695, 0.0, 0.019221924, 0.017602798, 0.017413978, 0.0, -0.07512528, -0.080483615, -0.066218294, 0.0) * g_13;\n    result += vec4(-0.010478934, -0.008364784, -0.010246552, 0.0);\n    return result + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Restore_CNN_S.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.19288683, -0.21397883, 0.111997396, -0.04791413, -0.26682988, -0.06144587, -0.03601853, -0.16693151, 0.038494494, -0.16651472, 0.147657, -0.083003886, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(-0.14286195, 0.08746566, -0.40107322, 0.12390977, -0.33392772, -0.18703035, -0.21326795, 0.04780781, -0.15155545, -0.0010025925, -0.1554875, -0.10676251, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(0.28095165, 0.022872915, -0.21342312, -0.29982176, 0.025937587, -0.055012174, -0.33779636, 0.0015666655, 0.076416336, 0.06656033, -0.1557806, 0.1078894, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(-0.31584853, 0.07527119, 0.30713862, -0.34014285, -0.50103146, -0.07217874, 0.512807, -0.09597398, -0.32097813, -0.051580857, -0.022466356, 0.01148551, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.026032459, -0.04193211, 0.37703893, -0.031916667, -0.27421117, 1.0906446, -0.049654085, -0.19814016, 0.07819544, 0.06003738, 0.1405805, -0.0064135445, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.041450135, 0.11319654, -0.23237701, 0.08443178, 0.53344345, 0.30857387, -0.057264958, -0.1575803, 0.2325609, -0.027797326, -0.04544767, -0.18720597, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.2531829, -0.074966915, -0.27800754, -0.3146097, 0.20126024, -0.5380133, -0.15082566, -0.19021043, 0.29951036, 0.17123336, -0.01681872, -0.12574998, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.25203633, 0.19882993, 0.14906439, 0.13593598, 0.40712556, 0.084902965, 0.42969635, 0.2961132, -0.057267334, -0.030388135, 8.8084314e-05, 0.0210724, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(-0.13459359, -0.12199573, 0.12591946, 0.24736497, 0.2033463, -0.09388599, -0.094370656, 0.1071285, -0.18479438, -0.066625565, 0.08279283, 0.20130983, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(-0.011108127, -0.07481861, 0.07640154, 0.4964964);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.056432575, 0.0028165397, -0.026325442, -0.14802271, 0.16885762, -0.062179096, -0.2332292, 0.17513658, -0.08011296, 0.02947316, 0.014771492, -0.17946689, 0.026012989, -0.09823925, 0.036625937, -0.06924322) * go_0(-1.0, -1.0);\n    result += mat4(-0.13571467, 0.09831142, 0.12911566, 0.06305893, -0.07188695, -0.20161287, 0.3858435, -0.21069056, -0.12294444, -0.1404628, -0.022659872, 0.23008968, 0.10969853, 0.17640765, 0.39796907, 0.20413099) * go_0(-1.0, 0.0);\n    result += mat4(-0.0061665224, 0.055102807, -0.0059629944, -0.021429887, 0.061626043, 0.16898955, -0.21215646, 0.16510476, 0.2238265, 0.19429931, 0.09874656, 0.06828208, -0.122404456, -0.00026717107, -0.28203064, -0.29979932) * go_0(-1.0, 1.0);\n    result += mat4(-0.22735378, 0.14538136, 0.11549746, 0.194148, -0.09841722, -0.0661309, 0.348576, -0.017375294, -0.044078812, 0.1298332, 0.04793373, -0.30687734, 0.08353025, 0.083519086, 0.10766399, 0.31796935) * go_0(0.0, -1.0);\n    result += mat4(0.048365135, -0.17566709, -0.33212858, -0.052667376, -0.26443407, -0.010216014, 0.1573303, 0.05725314, 0.08140953, -0.09664591, 0.076109104, -0.026773714, 0.07732627, 0.10188082, -0.28266954, -0.16230233) * go_0(0.0, 0.0);\n    result += mat4(0.29931107, 0.117944, -0.10414009, 0.12795551, 0.12576093, 0.17082554, -0.15803693, 0.13430743, -0.025801308, -0.10797019, 0.0721032, 0.2825884, -0.11025257, 0.12798019, 0.081827976, -0.050441865) * go_0(0.0, 1.0);\n    result += mat4(-0.11827391, 0.08306765, -0.3430314, 0.07898041, -0.023839617, -0.019507334, 0.23176382, -0.40992323, 0.09411734, 0.38415068, -0.25845516, -0.29984522, 0.1470966, -0.0684779, -0.07071314, -0.026773235) * go_0(1.0, -1.0);\n    result += mat4(0.19091596, 0.082110435, -0.5266589, -0.1744098, -0.015838385, -0.046316292, 0.023171103, -0.03731331, 0.2642396, 0.31824252, -0.041754793, -0.09525519, -0.14696182, 0.052168854, 0.039857205, -0.027555354) * go_0(1.0, 0.0);\n    result += mat4(0.15207373, 0.09845733, 0.0142631065, 0.096375965, 0.06089903, 0.17902578, -0.42391995, 0.22475442, 0.016356342, -0.06277531, -0.12173141, -0.18635495, -0.0013459618, 0.15725887, 0.019310836, 0.20293565) * go_0(1.0, 1.0);\n    result += mat4(-0.18395247, 0.30672902, 0.09034339, 0.1821889, -0.0419004, -0.2169228, -0.14052129, 0.11006559, 0.1709272, 0.51062274, 0.13758625, -0.2242552, -0.030382963, 0.3357568, -0.26491287, 0.02501938) * go_1(-1.0, -1.0);\n    result += mat4(0.040511727, 0.12523083, -0.27318433, 0.08388512, 0.25354835, 0.3404216, -0.2632471, -0.17784123, 0.2732347, 0.4468553, 0.084667034, -0.1856242, 0.034099877, -0.00954992, -0.32751867, -0.062207516) * go_1(-1.0, 0.0);\n    result += mat4(0.17564747, 0.11645554, -0.16362113, 0.105654195, -0.2762563, -0.1413764, 0.23264363, -0.14000498, 0.095402054, 0.0715738, -0.19346157, -0.028285999, 0.009799127, 0.04059529, 0.19688335, 0.1282381) * go_1(-1.0, 1.0);\n    result += mat4(0.23575781, -0.11446148, -0.20504695, 0.035568226, 0.36890212, -0.85968876, -0.18545328, 0.33796397, -0.30916876, -0.10445518, -0.3046253, 0.33271998, -0.06263589, -0.2160114, -0.16383372, -0.31173357) * go_1(0.0, -1.0);\n    result += mat4(0.20469664, 0.4039374, -0.070057206, 0.030353077, 0.39843914, -0.15490077, -0.24476516, 0.38238233, -0.21809858, 0.23496576, -0.051794037, 0.033664484, -0.14411364, -0.2515329, 0.124655396, -0.05818785) * go_1(0.0, 0.0);\n    result += mat4(-0.09065731, -0.16787091, 0.013269188, 0.23687351, -0.41504318, -0.048163068, 0.31760025, -0.33648986, 0.29752317, 0.2926866, 0.14408836, -0.33382463, -0.15873958, -0.121961035, 0.11797893, 0.09000567) * go_1(0.0, 1.0);\n    result += mat4(0.13356976, 0.013763947, 0.012169505, -0.109594524, 0.03417223, 0.7031121, 0.65146804, 0.5250268, -0.50132495, -0.419648, 0.2940041, 0.83051753, -0.17595838, 0.1633008, -0.018587278, 0.079596795) * go_1(1.0, -1.0);\n    result += mat4(0.07570128, -0.1581438, 0.03904949, 0.14890033, -0.054611947, 0.17469402, -0.44252598, 0.036181703, -0.4981031, -0.37507218, -0.18466389, 0.2645845, 0.25189674, -0.025896115, 0.034307647, -0.020462232) * go_1(1.0, 0.0);\n    result += mat4(-0.11645865, 0.02296537, 0.040909223, 0.015069485, 0.062284566, -0.22526766, 0.09241534, -0.32623053, 0.18208642, 0.3954284, 0.2884468, -0.25137675, -0.037232924, -0.10185309, -0.17956531, 0.018966453) * go_1(1.0, 1.0);\n    result += vec4(-0.16371979, -0.024620198, -0.035754893, 0.04176776);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.01921286, -0.26684764, -0.12663573, 0.31641877, -0.25313398, 0.12264074, 0.58750325, -0.14084283, 0.5837018, -0.042300556, -0.20435576, -0.009954825, 0.060783498, 0.05540401, 0.2205112, -0.06578902) * go_0(-1.0, -1.0);\n    result += mat4(-0.21930243, -0.03774968, 0.22615197, 0.18338196, 0.011201461, -0.271034, 0.00573116, -0.12248194, 0.47990513, 0.2982416, -0.1087603, -0.050099242, -0.07620939, -0.07148229, 0.03691984, -0.16796488) * go_0(-1.0, 0.0);\n    result += mat4(-0.14962853, -0.053769328, 0.02387081, 0.22002189, 0.052237745, -0.26160842, -0.08603077, 0.012542448, 0.08119985, 0.075785555, -0.33437458, -0.43373227, -0.13206963, -0.08759176, -0.03288923, -0.09799959) * go_0(-1.0, 1.0);\n    result += mat4(-0.1305593, -0.5974288, 0.06058367, 0.08406488, 0.013692483, 0.06646377, 0.16469325, 0.08990975, 0.42217395, -0.11289523, -0.06165009, 0.48556912, -0.15702641, -0.19922857, -0.0035429662, -0.0022089656) * go_0(0.0, -1.0);\n    result += mat4(-0.1964807, 0.038099788, 0.21587034, 0.039734077, -0.07063389, 0.11604167, -0.24558097, -0.08900199, -0.7684516, -0.1037487, -0.09380674, 0.33144563, -0.16653742, 0.0028585843, -0.33774406, -0.0528696) * go_0(0.0, 0.0);\n    result += mat4(-0.27298656, -0.05665099, 0.09661685, 0.19780266, 0.1025106, -0.22055034, -0.21218458, -0.040628925, 0.0095010325, 0.13118382, -0.42582452, -0.22197723, 0.21006055, -0.06189587, -0.15285942, -0.09526762) * go_0(0.0, 1.0);\n    result += mat4(-0.14494462, -0.046788953, 0.065877035, 0.09911713, 0.35096622, 0.16682479, 0.028363144, 0.36037162, 0.29413632, 0.28212717, -0.025364442, -0.3406269, 0.047262143, -0.11892685, -0.008032766, 0.29743317) * go_0(1.0, -1.0);\n    result += mat4(-0.15191558, -0.36980554, 0.14555687, 0.0043930537, -0.012661432, 0.15737776, -0.115250416, 0.10324491, 0.24491951, -0.15575431, -0.27802598, 0.21959937, 0.18063772, 0.4455559, -0.09693302, 0.33382267) * go_0(1.0, 0.0);\n    result += mat4(0.2717801, 0.13452889, 0.14105384, 0.16324317, -0.40111846, 0.1154301, -0.0076733204, -0.09697362, 0.44306824, -0.02831414, -0.2153124, -0.12075326, 0.060776163, 0.30347148, -0.0036976219, -0.12070682) * go_0(1.0, 1.0);\n    result += mat4(-0.39780128, -0.29875937, -0.12952097, 0.080333896, 0.07520163, 0.021689568, -0.23121156, -0.038140096, -0.1593877, 0.017156163, -0.06038025, 0.009244022, -0.13917233, 0.30957314, 0.243109, -0.104947075) * go_1(-1.0, -1.0);\n    result += mat4(-0.07965157, 0.06776501, -0.13288979, 0.005851189, -0.08768168, -0.03689969, 0.12034646, 0.22441491, 0.14453568, -0.17648841, -0.3378289, -0.018329712, 0.11722939, -0.34161824, 0.08424494, -0.01400687) * go_1(-1.0, 0.0);\n    result += mat4(0.08153887, 0.07222914, -0.14663404, -0.038526025, -0.07385973, 0.18440577, 0.35890242, 0.17084727, 0.26345527, 0.15280858, -0.007446105, -0.024403179, -0.30336383, -0.22978698, 0.11612946, -0.23614909) * go_1(-1.0, 1.0);\n    result += mat4(-0.07447396, 0.09023449, -0.13798, -0.086943336, -0.30787337, 0.15087669, 0.14418626, -0.03371195, 0.048989657, -0.13075387, -0.13458036, -0.059836224, 0.06495196, 0.269715, 0.3674355, 0.38956037) * go_1(0.0, -1.0);\n    result += mat4(0.34981915, -0.048779126, 0.31717536, 0.38080826, -0.20149232, -0.82969636, -0.10167862, 0.6382858, 0.25976858, 0.4370118, -0.04724865, -0.10014156, 0.19380626, -0.080370255, 0.09578106, -0.035166856) * go_1(0.0, 0.0);\n    result += mat4(-0.026443917, 0.4132611, 0.01822534, 0.12742202, -0.26652107, -0.2996705, 0.30905882, 0.07989903, 0.38249823, 0.21486135, 0.025314959, -0.14717339, -0.13344015, -0.32088286, -0.2833883, -0.30973712) * go_1(0.0, 1.0);\n    result += mat4(0.021517841, 0.006556378, 0.2025686, -0.12044382, -0.38583103, -0.0027515136, -0.06556736, -0.097090125, 0.04676486, -0.11954886, -0.051612873, 0.07831412, -0.18823163, -0.16542958, 0.04245155, 0.6437998) * go_1(1.0, -1.0);\n    result += mat4(-0.39475346, -0.2936861, 0.26768062, -0.28151843, 0.21935691, 0.2101108, -0.15455097, 0.19548604, 0.09188909, -0.020147726, 0.103328265, -0.12574542, -0.34167948, 0.07523185, -0.17669058, 0.62446547) * go_1(1.0, 0.0);\n    result += mat4(-0.37661025, -0.29630858, 0.05451026, 0.1611643, 0.14079669, -0.2170294, -0.038716137, 0.13514164, -0.21235192, -0.07860726, -0.005749412, 0.025625167, -0.13297133, 0.33012658, -0.27434957, -0.18416783) * go_1(1.0, 1.0);\n    result += vec4(-0.0036821906, -0.050239526, -0.01355402, 0.00048220603);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-3x3x3x8\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_2_tf\n//!SAVE MAIN\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.15873, 0.17989138, 0.14648493, 0.0, -0.017379675, -0.017363746, -0.019855022, 0.0, 0.009670625, 0.0070157526, 0.0075994316, 0.0, 0.025388412, 0.027231036, 0.024052646, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(0.048195973, 0.041760173, 0.037366055, 0.0, -0.115950756, -0.12887983, -0.12535639, 0.0, 0.032125086, 0.03397254, 0.032950625, 0.0, 0.01223746, 0.020822672, 0.0161561, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(0.0890567, 0.094453335, 0.09014035, 0.0, 0.016081346, 0.017434116, 0.020783134, 0.0, -0.011775135, -0.010094134, -0.018522855, 0.0, 0.072103254, 0.07940666, 0.065876864, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(-0.04841196, -0.06963968, -0.056574684, 0.0, 0.10912542, 0.11813441, 0.10643838, 0.0, -0.013013885, -0.01562045, -0.013802797, 0.0, 0.037505716, 0.04352026, 0.04645123, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.3472869, -0.36243078, -0.33530185, 0.0, 0.23654196, 0.2305048, 0.22150646, 0.0, -0.045226905, -0.041799217, -0.042511635, 0.0, -0.10267792, -0.1123385, -0.10845448, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.011987401, 0.012285043, 0.007813165, 0.0, -0.15911353, -0.17523928, -0.1535267, 0.0, 0.15675929, 0.16531634, 0.15948962, 0.0, -0.09240023, -0.09513292, -0.084187366, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.069052905, 0.07278333, 0.0756627, 0.0, -0.012180326, -0.018794727, -0.031050753, 0.0, -0.044663202, -0.04362803, -0.038904265, 0.0, -0.008540197, -0.011201734, -0.01556625, 0.0) * go_0(1.0, -1.0);\n    result += mat4(-0.08261173, -0.09042543, -0.07589266, 0.0, 0.043515377, 0.045066774, 0.04037769, 0.0, -0.06262993, -0.07469342, -0.058593787, 0.0, 0.026696987, 0.028740842, 0.037405368, 0.0) * go_0(1.0, 0.0);\n    result += mat4(0.07975598, 0.09597654, 0.08997132, 0.0, -0.07844719, -0.07880916, -0.06835411, 0.0, 0.05668995, 0.050163813, 0.053357534, 0.0, -0.020040333, -0.019867316, -0.01907621, 0.0) * go_0(1.0, 1.0);\n    result += mat4(-0.017078733, -0.017393313, -0.008266595, 0.0, -0.0033478448, -0.0027439648, -0.0042334674, 0.0, -0.06354017, -0.062058125, -0.04652064, 0.0, -0.010787706, -0.0062706997, -0.007573461, 0.0) * go_1(-1.0, -1.0);\n    result += mat4(-0.019895451, -0.016341688, -0.008712399, 0.0, 0.026231976, 0.023955572, 0.0216376, 0.0, -0.061950512, -0.05481285, -0.05261985, 0.0, -0.018804235, -0.016235247, -0.0131616965, 0.0) * go_1(-1.0, 0.0);\n    result += mat4(-0.055628926, -0.063315354, -0.057192408, 0.0, -0.0256364, -0.028660972, -0.02937357, 0.0, -0.017604912, -0.020851422, -0.016070362, 0.0, -0.0870202, -0.0832279, -0.07525406, 0.0) * go_1(-1.0, 1.0);\n    result += mat4(0.062738225, 0.07106593, 0.061644047, 0.0, -0.06068257, -0.06983662, -0.066070385, 0.0, 0.024919355, 0.03227179, 0.028569462, 0.0, -0.07866227, -0.098967604, -0.092128105, 0.0) * go_1(0.0, -1.0);\n    result += mat4(0.040397774, 0.047241107, 0.03962998, 0.0, -0.09112752, -0.10057507, -0.09301817, 0.0, 0.10833967, 0.101835825, 0.10027467, 0.0, 0.27189335, 0.27433604, 0.26781923, 0.0) * go_1(0.0, 0.0);\n    result += mat4(-0.044211388, -0.042373534, -0.03658007, 0.0, 0.113148406, 0.12423258, 0.107804194, 0.0, -0.17081551, -0.18562958, -0.17475435, 0.0, 0.09636739, 0.10763415, 0.093332425, 0.0) * go_1(0.0, 1.0);\n    result += mat4(-0.03798545, -0.047811143, -0.050768293, 0.0, 0.018775463, 0.026812987, 0.03452908, 0.0, 0.0055677597, 0.0039081173, -0.0017878668, 0.0, -0.10728597, -0.12618187, -0.109045394, 0.0) * go_1(1.0, -1.0);\n    result += mat4(0.06359783, 0.064184755, 0.04934199, 0.0, -0.009819327, -0.006616115, -0.007431496, 0.0, 0.025055679, 0.024787048, 0.017360551, 0.0, -0.047140837, -0.061695747, -0.06440822, 0.0) * go_1(1.0, 0.0);\n    result += mat4(0.060199022, 0.06482763, 0.059514645, 0.0, 0.026998974, 0.028776823, 0.024897143, 0.0, 0.17968474, 0.19337215, 0.16760105, 0.0, 0.0075838566, 0.010503482, 0.011993149, 0.0) * go_1(1.0, 1.0);\n    result += vec4(-0.0052927984, -0.0060193934, -0.0048643993, 0.0);\n    return result + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Restore_CNN_VL.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(0.1690102, -0.2560719, 0.39658326, -0.3679659, -0.27616683, -0.35619372, -0.3748396, 0.08430813, -0.29574734, -0.31511316, -0.09773105, 0.13616018, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(-0.1326393, -0.259433, 0.025070239, 0.58914864, -0.036478516, 0.30723435, 0.007458902, 0.012962684, 0.2493056, 0.13007334, -0.08448256, -0.38414413, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(-0.11539356, 0.35253766, 0.26143202, 0.2760807, -0.09371543, -0.028165473, -0.028452158, -0.27050856, 0.06718067, -0.0056619495, -0.17654495, 0.17288211, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(-0.16145481, -0.3204927, -0.54317135, 0.11830119, 0.49315026, 0.12008072, 0.50857407, -0.30382085, 0.25807253, 0.020755528, 0.29388228, 0.106109895, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.22728722, 0.50484747, -0.07904469, 0.33114597, 0.50306976, -0.22760947, 0.14773269, 0.17628263, 0.14788547, -0.08223464, -0.10880935, -0.3151985, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.3414351, 0.057279214, -0.14419858, 0.09761111, -0.11794496, 0.021717256, -0.22750235, 0.13986664, -0.38932344, 0.28996095, 0.3773904, 0.13175532, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.1376552, -0.19587159, -0.35147396, -0.097646296, 0.1686707, -0.14385861, 0.031198, 0.12383533, -0.23089902, 0.08707301, 0.3362293, -0.100579016, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(-0.056774966, 0.047585852, -0.36395878, -0.20211312, 0.4077735, 0.12631284, 0.39813092, -0.033365678, 0.2307249, -0.09131807, 0.20823865, 0.31084216, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(-0.12456089, 0.09755632, 0.31490886, -0.06579996, -0.13386595, 0.07564795, -0.26605195, -0.075180635, -0.11182657, 0.06757017, -0.14351276, -0.16828312, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(-0.046043985, 0.055581126, -0.08791638, -0.13022089);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf1\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.15485518, -0.29363206, -0.22610365, -0.14291525, -0.45240572, -0.18319772, -0.12209436, 0.15031648, 0.09878383, 0.06711082, 0.25763842, -0.084633484, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(-0.10204406, 0.16167697, 0.22371867, -0.37947702, -0.24476196, -0.038824454, 0.060157117, 0.15764871, -0.08072927, -0.2210841, -0.31835055, 0.009979876, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(0.20506924, 0.21132155, -0.0922578, -0.07430473, 0.14529926, 0.20549752, 0.0077948375, 0.13246094, -0.32353187, 0.21074104, 0.092629515, 0.17590871, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(0.04125819, -0.44050243, 0.23729716, 0.3218237, 0.12943116, -0.011674174, 0.10390632, 0.027775545, -0.20308031, -0.16904089, -0.2121676, -0.022515794, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(0.09664124, 0.20127031, 0.60345304, 0.16697013, 0.23093723, -0.38116834, 0.109695725, 0.0007595324, 0.4092646, 0.009624758, 0.11229678, 0.25326383, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.014879592, 0.19204311, 0.07102085, -0.7312604, 0.34860876, 0.3429918, -0.027331594, 0.27636307, 0.1342437, 0.107820466, -0.12645108, 0.21081445, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(-0.12687613, -0.09247973, -0.25973785, 0.4350873, -0.18987224, 0.028678741, -0.0903819, -0.63974863, 0.205591, 0.11308998, 0.18458389, -0.4149041, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.34691808, -0.025498383, 0.3428986, 0.21663484, 0.23404741, -0.1725327, -0.0036315925, -0.13299675, -0.1873967, 0.031331502, -0.08785591, -0.0013278709, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(-0.35846514, 0.048703704, -0.104165934, 0.16529736, -0.15378916, 0.26030356, -0.07134151, 0.03692383, -0.15807101, -0.18885155, 0.044707954, -0.11444462, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(-0.0022791293, -0.024132347, -0.57621074, 0.028573977);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.010346764, 0.07230188, -0.24734616, -0.09937907, 0.02228549, -0.19550583, -0.019540425, -0.1037373, 0.033996485, -0.075554, -0.20228972, 0.07090153, -0.09194035, -0.058972966, 0.1768268, 0.27517542) * go_0(-1.0, -1.0);\n    result += mat4(0.020078976, 0.12433655, -0.1620775, 0.036401592, 0.079748705, 0.11660013, 0.17917652, -0.017513236, -0.18936846, 0.24478136, -0.45726213, -0.045004416, -0.08295188, 0.067733586, -0.080548316, 0.2744211) * go_0(-1.0, 0.0);\n    result += mat4(0.024916803, 0.27562472, 0.043771956, -0.012240604, 0.0786355, 0.042651594, 0.16049327, -0.14577515, -0.032735053, 0.17658092, 0.16382934, -0.02337374, 0.11551492, 0.056343183, -0.17930213, 0.14259394) * go_0(-1.0, 1.0);\n    result += mat4(0.20010485, 0.06747722, -0.19026905, 0.11013709, 0.13062745, -0.044626113, -0.0062261797, 0.2189639, 0.1403497, -0.022713251, -0.19452858, -0.010305412, -0.06407589, 0.09836748, 0.025805516, 0.23430973) * go_0(0.0, -1.0);\n    result += mat4(-0.14664203, 0.034910418, 0.024714258, -0.066872925, -0.15717538, -0.14179383, -0.14091893, 0.05859166, 0.18919097, -0.18544437, -0.09068573, -0.08615929, -0.051434122, 0.2170678, 0.18409058, -0.17461225) * go_0(0.0, 0.0);\n    result += mat4(-0.11354446, 0.10745854, 0.2682663, 0.05949201, -0.10695986, 0.1407851, -0.03551388, 0.10691649, -0.17148238, -0.38287184, 0.2074456, 0.11828914, 0.048535194, 0.1464864, -0.18169662, -0.14074169) * go_0(0.0, 1.0);\n    result += mat4(0.22160622, -0.1513045, -0.053284165, 0.033202525, 0.15574448, -0.043640967, -0.0093824165, -0.0019965349, -0.097964935, -0.08289824, 0.08239996, 0.07868361, 0.05731752, -0.20441617, -0.013016076, -0.253108) * go_0(1.0, -1.0);\n    result += mat4(-0.031249097, -0.2272863, 0.23573665, 0.03357689, 0.011395065, -0.10885564, -0.06287508, -0.031719524, 0.10331069, 0.17560169, 0.18303394, 0.022961004, -0.17011635, -0.24371737, 0.10678694, -0.3222825) * go_0(1.0, 0.0);\n    result += mat4(-0.1275465, -0.08844758, 0.10994917, -0.00910273, 0.09393154, 0.03894992, 0.14367905, -0.11811715, -0.09077633, -0.015776094, 0.27427456, -0.13283503, 0.18724327, -0.08139094, 0.04933602, -0.051852766) * go_0(1.0, 1.0);\n    result += mat4(-0.06764611, -0.27426586, 0.12045272, 0.09410856, -0.14258035, 0.11802992, -0.09093882, 0.0022018093, 0.4590643, 0.046258576, -0.07827223, 0.448011, -0.103631735, -0.016930219, -0.15421398, 0.11045997) * go_1(-1.0, -1.0);\n    result += mat4(-0.17295076, 0.00151352, 0.14938255, 0.08336512, -0.07496541, -0.07561223, -0.0846474, 0.14979269, -0.09142163, 0.23925088, -0.015199518, -0.37749895, -0.20636298, -0.022585187, -0.20371509, 0.0745308) * go_1(-1.0, 0.0);\n    result += mat4(0.06458832, -0.009722021, -0.123604394, 0.06548835, -0.3039139, -0.022024399, 0.05297587, -0.0626883, 0.23556642, 0.1516464, -0.07004877, -0.1845364, -0.05918428, 0.19158973, -0.14983447, 0.030489758) * go_1(-1.0, 1.0);\n    result += mat4(0.36604697, 0.17516142, -0.10853731, -0.22694224, -0.107650936, 0.23013335, 0.094055794, -0.17047717, -0.3006048, -0.08621717, -0.18815655, -0.03570218, 0.09676118, -0.017718751, 0.059138596, 0.073388465) * go_1(0.0, -1.0);\n    result += mat4(-0.12791575, 0.101956226, 0.13091874, -0.046373338, 0.04955811, -0.04030444, 0.13869923, -0.046699073, -0.42611042, -0.7173929, 0.052184317, 0.6178025, -0.02929954, -0.07638965, -0.15000828, 0.030710017) * go_1(0.0, 0.0);\n    result += mat4(0.057806686, 0.20842272, -0.20148766, 0.006666912, 0.13356528, -0.45265228, -0.07354092, 0.21447696, 0.019552143, -0.13645506, 0.14643854, -0.0071413796, -0.15487236, -0.002250615, 0.30622452, 0.0033902125) * go_1(0.0, 1.0);\n    result += mat4(0.06896002, 0.24397352, -0.06479052, 0.20676947, -0.24259068, 0.055320013, -0.09032122, -0.11222854, -0.08982342, -0.114818625, -0.06399291, -0.3024516, -0.06302166, -0.1925528, 0.03458982, 0.028828239) * go_1(1.0, -1.0);\n    result += mat4(0.09764086, 0.09599894, -0.0073313303, 0.14418933, -0.045712367, 0.12657364, 0.04620374, -0.069778584, 0.30047333, -0.012418192, 0.15516461, -0.18087754, 0.08178273, 0.14262857, -0.01741533, -0.12509112) * go_1(1.0, 0.0);\n    result += mat4(0.04697884, -0.1506804, 0.031823065, 0.13397239, -0.18396698, 0.10681781, -0.29586303, -0.0039136545, 0.17560847, -0.12486726, -0.018646788, -0.20688744, -0.030614454, -0.0527634, 0.23593572, -0.10542146) * go_1(1.0, 1.0);\n    result += mat4(-0.19182229, -0.32615846, 0.26283535, -0.1371942, -0.071202695, 0.12056063, -0.11450658, -0.27711076, -0.42096004, 0.0014352369, 0.1559669, -0.14464542, -0.17973948, 0.079166576, -0.12501791, -0.20623216) * go_2(-1.0, -1.0);\n    result += mat4(0.12469872, 0.32190827, -0.059510354, 0.1393449, -0.12845798, -0.019571869, -0.22630808, -0.14031963, 0.36072046, 0.05858427, 0.19278921, 0.121090546, -0.067538865, -0.018770566, 0.14318037, -0.15561756) * go_2(-1.0, 0.0);\n    result += mat4(0.024663208, 0.21110268, -0.016415706, 0.060093414, -0.03739678, -0.107412934, -0.077527136, 0.30331334, 0.17196326, -0.15512557, -0.09499732, -0.15748607, -0.16680105, -0.015185634, 0.16114107, -0.21288376) * go_2(-1.0, 1.0);\n    result += mat4(-0.17739037, -0.1190967, 0.13191372, -0.2527187, -0.14992718, -0.30511454, 0.19145966, 0.002194003, -0.12888977, 0.19152176, 0.27528167, 0.099714965, 0.12865707, -0.12051514, -0.055013947, 0.26231763) * go_2(0.0, -1.0);\n    result += mat4(0.46433613, -0.11708138, -0.20157282, 0.32022122, 0.079468675, 0.029407484, 0.2559102, -0.15651533, 0.08644574, -0.09747344, -0.07528584, 0.17354868, 0.19167562, -0.17698488, -0.09896657, 0.17093097) * go_2(0.0, 0.0);\n    result += mat4(0.20283653, -0.33680332, 0.2282385, 0.18832158, 0.20866042, 0.00076752366, 0.16471444, -0.21548858, 0.16193539, 0.17141372, 0.03140222, 0.03913644, -0.030161971, 0.00014570929, 0.08993654, -0.064823024) * go_2(0.0, 1.0);\n    result += mat4(-0.3075755, 0.19942546, 0.015526995, -0.120868504, -0.254515, -0.07791228, 0.03271691, 0.11794217, 0.11258601, 0.045204375, -0.061196107, -0.115958795, 0.3861869, 0.048215542, 0.07016682, -0.009975758) * go_2(1.0, -1.0);\n    result += mat4(-0.07623697, 0.16094944, -0.02283455, 0.14112763, -0.051149167, 0.20429814, 0.011314802, 0.18914083, -0.24240434, -0.08784008, -0.16763984, -0.08492233, 0.31062725, -0.11925119, -0.33195966, 0.2060798) * go_2(1.0, 0.0);\n    result += mat4(-0.016709225, -0.14472668, -0.3677625, -0.09832719, 0.030297454, -0.05775362, -0.1401375, 0.08119674, -0.01795042, 0.05183797, -0.24320887, 0.066842034, -0.22245285, -0.02740993, 0.06316751, 0.053399116) * go_2(1.0, 1.0);\n    result += mat4(-0.039214406, -0.08876633, 0.045552462, 0.19226661, 0.1355001, -0.13942362, 0.17398876, 0.2914014, -0.191809, 0.037143208, 0.013333581, -0.16632195, 0.113767646, -0.106692605, 0.1589787, 0.030107044) * go_3(-1.0, -1.0);\n    result += mat4(0.21997562, 0.13855208, -0.05783191, -0.033682413, -0.010961168, 0.10524961, 0.02177416, 0.18289444, 0.043692037, 0.07853899, -0.039936125, -0.1004449, 0.04494073, -0.020680292, 0.17578089, -0.106598996) * go_3(-1.0, 0.0);\n    result += mat4(0.026852835, -0.16037546, 0.11278316, 0.12656097, -0.006857894, -0.03400118, -0.051564034, 0.00085412664, -0.37556714, -0.05279987, 0.029383834, -0.14246808, -0.056380164, -0.002399925, 0.16025752, 0.036324855) * go_3(-1.0, 1.0);\n    result += mat4(0.022709966, 0.046350412, 0.03390721, 0.02810572, -0.14394265, 0.04215361, -0.3206118, 0.15034916, -0.0028448137, 0.1682989, -0.042686664, 0.020543462, -0.2786501, -0.007482015, -0.040313292, -0.20745736) * go_3(0.0, -1.0);\n    result += mat4(0.05417556, 0.18728684, -0.046121832, -0.27939513, 0.05907976, -0.09191223, -0.16625418, -0.26038164, 0.39956605, -0.052594025, -0.0596556, 0.29517552, -0.015181923, -0.0763375, 0.25131205, 0.13038464) * go_3(0.0, 0.0);\n    result += mat4(-0.036903054, -0.0066989153, -0.062650286, 0.05614359, -0.0064960583, 0.028512698, -0.10906273, -0.010047654, 0.23030473, 0.049983572, 0.10439064, 0.26643834, 0.05041243, 0.09185424, -0.32352915, 0.11295159) * go_3(0.0, 1.0);\n    result += mat4(0.09724027, -0.34962535, 0.06586686, 0.016635379, 0.13831381, 0.01707076, -0.04690347, 0.022350075, 0.018352794, 0.022000022, 0.070613205, 0.117735535, -0.025971051, 0.18832101, -0.09643588, -0.08512127) * go_3(1.0, -1.0);\n    result += mat4(-0.17324433, 0.06810613, -0.057295907, -0.05115964, -0.101570815, 0.12491774, 0.08762367, -0.005862404, -0.05342927, -0.031942457, -0.039624047, -0.04298937, -0.1303138, -0.11869282, -0.024832053, 0.070463404) * go_3(1.0, 0.0);\n    result += mat4(-0.010514842, 0.1376259, -0.11750346, -0.03786737, 0.03459249, 0.015408171, -0.031430878, -0.060825355, -0.072958425, -0.0037895301, 0.041686177, -0.12352204, -0.06261361, 0.054514423, -0.34072715, 0.13860728) * go_3(1.0, 1.0);\n    result += vec4(0.018166734, -0.11002478, -0.05554318, -0.0988193);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!SAVE conv2d_1_tf1\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.040142782, 0.0288423, 0.07569487, -0.01490842, 0.14402796, -0.13682005, 0.027765118, 0.03907358, 0.07117706, 0.058157545, -0.23862502, -0.057674367, -0.19220531, 0.0147159435, -0.18028538, 0.0963821) * go_0(-1.0, -1.0);\n    result += mat4(-0.1676744, -0.11937339, 0.12137117, 0.07119485, 0.14148116, -0.043578617, -0.029261118, -0.0016938087, -0.057269357, -0.080076694, 0.12193026, 0.07326153, -0.056278303, -0.01630716, -0.03792076, 0.1483611) * go_0(-1.0, 0.0);\n    result += mat4(-0.3021578, 0.011601693, 0.11266048, 0.19086999, -0.0122412145, 0.08431291, 0.11615175, -0.008039614, -0.39987534, 0.07820729, 0.03509667, 0.1963505, -0.08839513, -0.21571854, 0.059425723, -0.06830175) * go_0(-1.0, 1.0);\n    result += mat4(0.23135209, -0.12452708, 0.0943565, 0.0028859286, -0.09836373, 0.10681712, -0.3535964, 0.08457615, 0.045332734, 0.16579892, -0.03809797, -0.021596594, 0.2937497, -0.028294371, 0.046484597, -0.037604347) * go_0(0.0, -1.0);\n    result += mat4(0.072675414, -0.16431206, 0.28952035, 0.0076831076, -0.020242939, 0.029483542, -0.092415355, 0.08673106, 0.12109694, 0.14307201, 0.23134442, 0.11731775, 0.09981601, -0.16968462, 0.037470713, 0.14948717) * go_0(0.0, 0.0);\n    result += mat4(0.0029752052, 0.06526503, 0.1866458, 0.07451277, -0.31836876, 0.17115082, -0.13969697, 0.23844297, -0.03244903, -0.08832665, 0.023691226, -0.18230624, -0.074933805, -0.00044301842, 0.050572682, 0.081511915) * go_0(0.0, 1.0);\n    result += mat4(0.039502528, 0.051221415, -0.13968123, -0.091212444, -0.016925618, 0.15409444, -0.017455677, -0.11653652, 0.03539446, -0.00087720866, -0.12839639, 0.037198763, 0.03674469, -0.26444665, 0.019721227, -0.13013805) * go_0(1.0, -1.0);\n    result += mat4(0.039229527, 0.25667152, 0.0032586441, -0.00718359, 0.1617932, 0.10409968, 0.07182867, -0.09810605, 0.07789241, -0.02014911, 0.025767172, -0.14604759, 0.07175764, 0.32513744, -0.20473222, -0.16266066) * go_0(1.0, 0.0);\n    result += mat4(0.13418433, 0.061813723, -0.13927278, -0.2498272, 0.03468218, 0.29483125, 0.063289374, -0.04726235, 0.1898295, -0.33132064, 0.032045014, 0.02159535, -0.1148363, 0.31306976, 0.06456038, 0.048988886) * go_0(1.0, 1.0);\n    result += mat4(0.07151646, 0.2799246, -0.107190795, -0.16431166, -0.28007045, 0.07206954, 0.06775463, 0.009758042, 0.07032184, -0.20843789, 0.087045245, 0.1360676, -0.25718534, 0.028249472, -0.12614648, 0.009949602) * go_1(-1.0, -1.0);\n    result += mat4(0.020241471, -0.23390484, -0.0083223935, 0.08344701, 0.08222297, 0.12026539, -0.08652223, -0.08228822, -0.039576706, -0.24677879, -0.1157289, 0.2590508, -0.23809408, 0.19911982, -0.116798095, -0.035870325) * go_1(-1.0, 0.0);\n    result += mat4(0.024991842, 0.050509237, -0.024134455, -0.12659028, 0.24089767, 0.122712664, -0.10482493, -0.19403952, -0.19177693, -0.06538376, -0.041478425, 0.32176673, -0.1534002, -0.18680622, 0.06763643, 0.020806564) * go_1(-1.0, 1.0);\n    result += mat4(0.03437814, -0.28067374, 0.2830681, 0.038812317, -0.021698112, -0.120865285, 0.22695538, -0.045419116, -0.030475847, -0.01977341, -0.1265364, -0.3109814, 0.012255813, 0.053917278, -0.018620957, -0.14599285) * go_1(0.0, -1.0);\n    result += mat4(-0.016204128, -0.04093018, 0.054571863, 0.02679643, 0.01756274, -0.057685968, 0.16148666, 0.17370272, -0.11065411, 0.06378157, -0.09331551, 0.22985275, 0.057905316, 0.12323568, 0.07748665, 0.09878629) * go_1(0.0, 0.0);\n    result += mat4(-0.018112244, 0.063234635, -0.013184602, 0.16241394, 0.08877139, 0.02145378, -0.02490027, -0.038920373, 0.13127136, 0.14391647, 0.020553736, 0.14401346, 0.06685973, -0.25398204, 0.10369067, -0.055949755) * go_1(0.0, 1.0);\n    result += mat4(0.07710333, 0.047412727, 0.13813803, 0.18624061, 0.16907091, -0.039532468, 0.06234584, 0.06408178, -0.054543987, -0.045220226, -0.11093376, -0.37399602, 0.20372874, 0.004580967, -0.07742308, 0.017989937) * go_1(1.0, -1.0);\n    result += mat4(0.003485311, -0.08897399, -0.013108594, -0.19473282, -0.27081844, -0.16812073, 0.0052992934, -0.055331517, 0.09446357, 0.019280333, 0.16560757, -0.3230032, 0.043096773, 0.059222896, -0.064184934, -0.059852477) * go_1(1.0, 0.0);\n    result += mat4(0.06794279, -0.034135245, 0.083064295, 0.13506731, 0.13064219, -0.44978833, -0.03513717, 0.08999715, 0.1124541, 0.42208397, -0.0038724816, -0.014332087, -0.13751853, -0.04929869, 0.09134992, -0.17687531) * go_1(1.0, 1.0);\n    result += mat4(0.100909084, -0.0131197255, 0.082274795, -0.2138443, -0.08515947, -0.021058358, 0.10951775, -0.06349191, -0.29129833, -0.029262653, 0.25235432, -0.11748315, 0.121980384, 0.062347785, 0.10916932, -0.15993518) * go_2(-1.0, -1.0);\n    result += mat4(0.28893283, -0.05677308, -0.2641288, -0.058937225, -0.16187571, 0.006647366, -0.063294955, 0.04766719, 0.60601914, -0.07831864, -0.15710756, -0.011491797, 0.15587467, -0.08105375, 0.07847514, -0.2803333) * go_2(-1.0, 0.0);\n    result += mat4(-0.077989794, -0.09871811, -0.3516344, 0.15292728, 0.010889273, 0.0011189661, -0.16118282, -0.018821161, -0.039708678, -0.00060983415, -0.06367813, 0.009148068, 0.03919827, 0.18782744, 0.028040757, -0.10230145) * go_2(-1.0, 1.0);\n    result += mat4(-0.4079609, 0.18640275, -0.12475227, 0.13891742, 0.25121725, 0.16942379, 0.14409852, 0.087600805, 0.045335658, -0.12683709, -0.0077387216, 0.06563413, -0.19857128, 0.106910795, -0.048285246, 0.10768945) * go_2(0.0, -1.0);\n    result += mat4(0.5989075, 0.20941062, -0.20086494, 0.13344856, 0.073034994, 0.22358665, 0.101664364, -0.13463663, 0.18816395, -0.061176624, -0.14712185, 0.027320342, -0.09529667, 0.031148786, -0.28744993, 0.18698911) * go_2(0.0, 0.0);\n    result += mat4(0.14799193, 0.39471942, -0.23340325, -0.4031061, 0.18926248, -0.11091216, 0.118981816, -0.09155061, 0.17049436, 0.19803695, -0.1513267, 0.023817873, 0.0090933135, -0.04134864, 0.060486555, 0.03536634) * go_2(0.0, 1.0);\n    result += mat4(-0.39094314, 0.01779997, 0.12710269, 0.0067333193, -0.31255835, -0.08206612, -0.048528638, 0.369439, -0.19351655, -0.03420455, 0.15831526, -0.052294146, -0.08481741, 0.0787108, 0.1312136, -0.108919285) * go_2(1.0, -1.0);\n    result += mat4(-0.16068119, -0.42190582, 0.19383872, -0.018445708, 0.09803051, -0.020769652, -0.022599563, -0.052448895, -0.20645833, -0.031432863, 0.0025441595, 0.03410379, -0.20268854, 0.04481527, 0.05191063, 0.42317194) * go_2(1.0, 0.0);\n    result += mat4(-0.12786235, -0.23936178, 0.116561726, 0.30756372, -0.09420156, -0.044529166, -0.03585749, 0.1829332, -0.23939075, 0.24030831, 0.019878127, -0.015069802, 0.24300557, -0.22558568, -0.104956664, -0.09393648) * go_2(1.0, 1.0);\n    result += mat4(-0.04607054, 0.012677649, -0.027597688, 0.1618836, 0.29210827, 0.014221155, -0.13591036, -0.06895336, -0.09559534, 0.07956421, -0.11112994, -0.13325493, 0.24562472, 0.11046177, 0.057847694, 0.0016315983) * go_3(-1.0, -1.0);\n    result += mat4(-0.03365951, 0.027391057, 0.09653403, -0.14718771, -0.049631152, -0.06467214, -0.058545876, 0.1424002, -0.06320376, 0.181183, 0.10249362, -0.16052136, 0.3013475, -0.04156266, 0.08862033, 0.06888033) * go_3(-1.0, 0.0);\n    result += mat4(0.10045977, -0.004198456, -0.025856055, 0.05739418, -0.1328637, -0.025975171, 0.06553717, 0.11301186, 0.0704087, -0.083569765, 0.16066101, -0.24453588, 0.25370175, 0.037184533, 0.062386766, -0.20025635) * go_3(-1.0, 1.0);\n    result += mat4(-0.017958941, 0.06417776, -0.1525265, 0.12451173, 0.14567685, -0.0049682115, -0.23973411, -0.0783304, -0.010629432, 0.08055161, 0.2028341, 0.17640644, -0.20445108, -0.055524793, -0.019326134, 0.081288636) * go_3(0.0, -1.0);\n    result += mat4(0.007882519, -0.03722546, 0.053249408, 0.00071846246, -0.07053029, -0.21583866, 0.1415364, -0.19486657, 0.20685542, 0.17660026, -0.32156837, 0.1746825, -0.14957622, -0.09224378, -0.098153435, -0.13054638) * go_3(0.0, 0.0);\n    result += mat4(0.10051427, -0.17398237, 0.09842799, -0.14187703, 0.116901085, -0.1229543, -0.0007776771, -0.20410055, -0.11373484, -0.111150615, -0.1974002, -0.11641459, 0.024105398, 0.24985977, 0.015871854, -0.10724633) * go_3(0.0, 1.0);\n    result += mat4(-0.18081793, 0.1209351, -0.12867971, -0.019415248, 0.062617876, -0.037130393, -0.07803658, -0.22862352, 0.2586428, -0.030090366, -0.11894069, 0.18087515, -0.40921417, 0.070013195, 0.030540073, 0.035120826) * go_3(1.0, -1.0);\n    result += mat4(-0.13185939, 0.12992652, 0.08125049, 0.075331174, 0.064219765, 0.056629725, -0.020012032, -0.0855444, -0.044063166, -0.05396545, -0.028002812, 0.21837157, -0.15206428, -0.12681007, 0.14895032, 0.12339962) * go_3(1.0, 0.0);\n    result += mat4(0.08066341, -0.14773634, -0.0212227, -0.014011867, -0.048505764, 0.075407125, -0.020620076, 0.0003291325, -0.21815202, -0.23136546, 0.10853532, -0.036058456, 0.10952532, -0.052677035, -0.13005799, 0.18398996) * go_3(1.0, 1.0);\n    result += vec4(0.022609137, -0.028548084, 0.024431901, 0.010504478);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.069641694, 0.104958326, 0.14786446, 0.027633663, -0.004279524, -0.020451711, 0.0883571, -0.016224537, 0.13585235, 0.11078269, 0.20198658, -0.042161036, 0.020466218, 0.20994963, 0.20072585, -0.028024657) * go_0(-1.0, -1.0);\n    result += mat4(0.050872434, 0.12874635, 0.1298729, 0.115810685, 0.07087254, 0.09885682, 0.23018982, 0.19187538, 0.10953604, 0.0033836907, -0.13325337, 0.09830315, -0.06528767, 0.05096927, -0.016355392, -0.039334368) * go_0(-1.0, 0.0);\n    result += mat4(0.027010268, 0.018263958, 0.0360758, 0.016791478, 0.2815702, 0.15517488, 0.43415815, 0.044976447, -0.0070842914, -0.12546758, 0.16874593, 0.077622116, 0.02252915, 0.1769774, 0.07181055, -0.15128697) * go_0(-1.0, 1.0);\n    result += mat4(0.057129618, 0.118046716, 0.07237424, -0.07842637, -0.044214778, -0.12886304, 0.08603301, -0.10416606, -0.15852053, 0.3788151, 0.26181692, -0.09092249, 0.31635332, 0.064212754, 0.21923725, 0.07500004) * go_0(0.0, -1.0);\n    result += mat4(-0.16981383, 0.044409662, -0.3717617, -0.031610407, 0.03658662, -0.09459229, -0.09449437, -0.014000666, -0.19656453, 0.03934163, -0.16304104, -0.12761801, -0.06235523, 0.16438273, -0.036933117, -0.095564745) * go_0(0.0, 0.0);\n    result += mat4(0.09725091, 0.034022827, 0.17699842, 0.1079676, -0.13236652, 0.03718181, -0.06968635, -0.23288171, 0.10275666, 0.08464966, -0.37162134, -0.35782215, -0.11023659, 0.2519236, -0.035197742, -0.019324787) * go_0(0.0, 1.0);\n    result += mat4(-0.09968464, 0.01102193, 0.0073735216, 0.011999313, -0.004998707, 0.09518938, 0.045727003, -0.21544908, 0.006879454, -0.06398254, -0.12584935, -0.06759933, -0.0820037, -0.07775104, 0.021957919, -0.122708224) * go_0(1.0, -1.0);\n    result += mat4(-0.08869767, 0.031296413, -0.0034280645, 0.13778855, 0.10073061, -0.08393937, -0.032959275, -0.0500518, 0.010908757, -0.09189417, -0.057760105, 0.17652664, -0.08729078, -0.09639096, -0.25654703, 0.055152636) * go_0(1.0, 0.0);\n    result += mat4(0.0027847723, -0.12885433, 0.038065907, 0.17450769, 0.0864409, 0.04592345, -0.015443841, 0.077010944, 0.08967368, 0.06800111, -0.23636387, 0.35023567, 0.03165923, 0.03132063, 0.17964344, 0.035610788) * go_0(1.0, 1.0);\n    result += mat4(-0.032017227, -0.0022808525, -0.08470573, 0.05332408, -0.14674746, 0.025374275, -0.018281924, 0.041163016, 0.00096549373, 0.014724006, 0.004913065, 0.18494442, 0.034953076, -0.15731992, -0.13792977, 0.08041999) * go_1(-1.0, -1.0);\n    result += mat4(0.08305006, 8.6318905e-05, -0.007895379, 0.02731387, -0.061324496, 0.050034665, 0.22662131, -0.013876427, -0.074468784, -0.008136604, -0.23337875, -0.1742574, 0.011753501, -0.11666686, -0.22541048, -0.14549944) * go_1(-1.0, 0.0);\n    result += mat4(-0.028333234, 0.121047184, 0.06720256, -0.058930036, 0.030258363, 0.07292774, 0.06455556, 0.0019076486, 0.0073987027, 0.17144889, 0.06084024, -0.08762086, -0.114422195, -0.16595861, -0.08706028, -0.10736261) * go_1(-1.0, 1.0);\n    result += mat4(-0.02519315, -0.14611271, 0.0388848, 0.19481422, -0.05970354, -0.08391417, 0.18982239, -0.10447052, 0.15587378, -0.023997072, 0.0781739, 0.2182389, -0.023886079, -0.1422596, -0.13352804, 0.005008043) * go_1(0.0, -1.0);\n    result += mat4(0.08842712, -0.100292705, 0.18925671, 0.12198875, 0.061771665, -0.04473232, 0.025053164, 0.039047796, -0.1672479, -0.08934517, 0.33099812, -0.20269585, -0.21640155, -0.22029749, 0.16539703, -0.2442679) * go_1(0.0, 0.0);\n    result += mat4(-0.16332205, -0.101898365, 0.02919932, -0.11900455, 0.14442924, 0.0916815, 0.037550304, 0.024123482, 0.02042624, 0.033472955, -0.059437107, -0.18735693, -0.013749093, -0.06199881, -0.08685079, 0.04252364) * go_1(0.0, 1.0);\n    result += mat4(-0.09047013, -0.055188328, -0.09106191, -0.048969727, 0.05114009, -0.12753403, 0.07116141, 0.060749624, -0.074034564, -0.21952136, -0.09479503, 0.2753584, -0.014141759, -0.14883812, -0.0673838, -0.012279045) * go_1(1.0, -1.0);\n    result += mat4(0.013816464, -0.0747162, -0.19202435, -0.064403646, 0.34980014, 0.04375546, 0.20264609, 0.006684355, 0.11523799, 0.024674915, -0.08697566, -0.04662527, -0.12743855, -0.39463726, 0.0057380227, 0.01286557) * go_1(1.0, 0.0);\n    result += mat4(-0.08146522, 0.074080914, -0.16856177, -0.183158, 0.19228102, 0.12373886, 0.017574452, -0.01753772, 0.045071773, 0.07725093, 0.023422163, -0.011545186, 0.20751388, -0.10795588, 0.07606346, 0.10282933) * go_1(1.0, 1.0);\n    result += mat4(0.12512013, -0.102208994, -0.09125398, 0.12043188, -0.066011876, 0.08831903, -0.017038671, -0.005541508, -0.049607087, 0.08654939, -0.02037085, 0.26887566, 0.005012545, 0.01869507, -0.013064982, -0.010649147) * go_2(-1.0, -1.0);\n    result += mat4(0.006824864, -0.05071593, -0.20786697, -0.07327317, 0.011382597, 0.030494886, -0.04754353, -0.018284699, 0.01305972, -0.036589053, 0.26637617, 0.021887446, -0.026669119, -0.037982125, -0.063445956, -0.009104248) * go_2(-1.0, 0.0);\n    result += mat4(0.032602567, 0.07094331, 0.052653246, 0.08342047, -0.085082285, -0.14674088, -0.23073354, -0.07915851, 0.0017120204, 0.032407638, -0.039819505, 0.16942178, 0.023192152, -0.0353237, 0.10930186, 0.22939779) * go_2(-1.0, 1.0);\n    result += mat4(0.0010455973, -0.11821993, -0.12639599, 0.12250084, -0.12756817, 0.11478416, -0.1862587, 0.016819192, 0.02110181, -0.25492984, -0.1766048, 0.22188173, -0.21305011, 0.113442205, 0.04599144, -0.15840286) * go_2(0.0, -1.0);\n    result += mat4(-0.15086032, -0.17428935, 0.39080557, 0.07576757, 0.121703945, 0.17944208, -0.003140103, -0.11231332, 0.12102969, 0.15310267, 0.17578171, 0.40631834, -0.21299168, 0.024928993, 0.030104794, 0.020753227) * go_2(0.0, 0.0);\n    result += mat4(-0.098734386, -0.020072265, -0.14308836, -0.08490801, 0.017175158, 0.02250534, 0.04060829, 0.033022214, 0.0046218676, 0.17923212, 0.0112105915, 0.09574084, 0.14819936, -0.14692923, 0.12634254, 0.060762513) * go_2(0.0, 1.0);\n    result += mat4(0.030521613, -0.097913325, -0.016720278, 0.11273997, 0.013019863, -0.06557118, 0.0405774, 0.0915019, 0.022414956, -0.053254984, 0.18639986, 0.07820968, 0.06498986, 0.058922634, -0.02240318, -0.086019725) * go_2(1.0, -1.0);\n    result += mat4(0.2058775, 0.01502064, 0.05847032, 0.007249146, 0.086483665, 0.19420148, 0.03892261, -0.013546935, -0.07980237, 0.04347281, -0.10376214, -0.1366535, 0.05285337, 0.07213318, 0.3642818, -0.11331124) * go_2(1.0, 0.0);\n    result += mat4(-0.025740806, 0.14551482, -0.037410017, -0.17477523, -0.11853099, -0.060820814, -0.102599286, -0.13267937, -0.103053465, -0.014044828, -0.01888072, -0.06499249, 0.22311528, -0.051850274, -0.034120858, 0.044562567) * go_2(1.0, 1.0);\n    result += mat4(-0.21360217, 0.10093803, -0.0016407765, -0.1473997, 0.26524043, 0.02112132, 0.23173104, -0.013157391, 0.05945182, 0.044635538, 0.06031638, -0.21435826, -0.10147484, 0.069090195, 0.09641844, -0.09581093) * go_3(-1.0, -1.0);\n    result += mat4(-0.08576515, -0.122861005, 0.049567085, -0.085854456, 0.23809357, -0.024966082, -0.10294079, 0.046241313, 0.008621132, -0.08323767, 0.20277941, 0.163423, -0.07386535, -0.088738985, 0.05274358, -0.025479877) * go_3(-1.0, 0.0);\n    result += mat4(-0.041135542, -0.008365642, 0.17088248, 0.04025207, 0.13809255, -0.056895368, -0.01582834, 0.07361908, -0.00068995473, -0.09300962, 0.19117641, 0.24832036, -0.06572358, -0.026025, -0.019093119, -0.049720034) * go_3(-1.0, 1.0);\n    result += mat4(0.024900286, 0.11525501, 0.025882801, 0.037742402, 0.36976853, 0.052211333, -0.15143296, 0.1802276, -0.059080046, 0.017990451, 0.026395092, -0.12689115, -0.07705386, 0.1232379, 0.13273561, -0.12521964) * go_3(0.0, -1.0);\n    result += mat4(-0.19788785, 0.044887315, 0.07663442, 0.16688696, -0.2842248, -0.15684547, 0.028387763, 0.0063470444, -0.012245601, -0.038382255, -0.8187406, -0.25245667, 0.23014604, 0.22746666, 0.1594356, 0.16469443) * go_3(0.0, 0.0);\n    result += mat4(-0.12663333, 0.014730006, 0.03765697, 0.15704912, -0.106595434, -0.05317512, -0.081759915, -0.08797109, 0.064620756, -0.06341419, 0.16493447, 0.23102313, 0.068325415, -0.088058695, 0.16885915, 0.036382258) * go_3(0.0, 1.0);\n    result += mat4(0.035389822, -0.11811836, -0.035656307, -0.0680554, 0.1338908, 0.065852076, 0.023307983, 0.0675308, 0.09690683, 0.18170924, 0.09862692, -0.20964378, -0.08601271, -0.20016764, -0.01879598, -0.14629345) * go_3(1.0, -1.0);\n    result += mat4(-0.27183273, 0.013525998, -0.14995874, -0.23938845, -0.26218823, -0.0009874097, -0.13385512, -0.10664239, -0.048931994, 0.039898522, 0.047444753, 0.10934722, 0.10969629, 0.123539805, 0.11692802, 0.14172275) * go_3(1.0, 0.0);\n    result += mat4(-0.1656506, 0.019683002, 0.0221048, 0.12596753, 0.20420644, -0.07930122, 0.04653823, 0.11492255, -0.0050175437, -0.03271697, 0.013389486, 0.034583613, -0.2196601, -0.1615663, -0.013763388, -0.056037936) * go_3(1.0, 1.0);\n    result += vec4(-0.022956269, 0.029688787, -0.070148066, -0.07163476);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!SAVE conv2d_2_tf1\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.15104648, 0.05522861, -0.0654341, -0.053517453, -0.08264124, -0.0062249107, -0.20364265, -0.05015117, -0.18837251, 0.030655831, 0.046844713, -0.20673253, -0.14042036, -0.05655449, 0.13994302, 0.011745607) * go_0(-1.0, -1.0);\n    result += mat4(-0.16517559, 0.1489214, -0.09149559, 0.025003506, -0.124926426, 0.16974348, -0.020857265, 0.08017403, 0.21836148, 0.0025619378, 0.2331612, 0.085599184, -0.030934382, -0.055194855, 0.09527726, -0.10081552) * go_0(-1.0, 0.0);\n    result += mat4(0.041800212, 0.028859638, 0.09395546, 0.05211183, -0.038541477, 0.021495212, 0.04862346, -0.007864793, 0.038407274, -0.13841268, -0.14963801, 0.26470762, 0.16691841, -0.07262008, 0.034374326, -0.14709206) * go_0(-1.0, 1.0);\n    result += mat4(0.00094978884, -0.028974704, -0.0900548, -0.08401967, -0.08935931, -0.043606587, -0.14497143, -0.05226239, -0.21516493, 0.19410603, -0.089924194, -0.04335071, -0.012618276, -0.2671613, 0.020422975, -0.037739716) * go_0(0.0, -1.0);\n    result += mat4(-0.13403237, -0.02524383, -0.03474901, 0.054432765, 0.11946775, 0.107336655, -0.1431715, -0.13370377, 0.015087512, -0.1917613, 0.073493585, 0.2788855, -0.010510839, 0.06891479, -0.06741307, -0.05271205) * go_0(0.0, 0.0);\n    result += mat4(-0.15432046, 0.04021662, -0.16979513, 0.13660534, -0.10518303, -0.10095502, -0.13092068, 0.022805348, -0.16676381, -0.4273298, 0.020867536, 0.3506733, -0.29459694, -0.055828743, -0.069241956, 0.04106382) * go_0(0.0, 1.0);\n    result += mat4(-0.08890133, 0.07549666, -0.040735144, -0.1506932, -0.22227979, -0.0762723, -0.17766447, -0.05741318, -0.21885683, 0.2379157, -0.15525854, -0.07306285, 0.15580738, -0.04394069, -0.19175608, 0.018283797) * go_0(1.0, -1.0);\n    result += mat4(-0.08503275, -0.105500385, -0.114987396, -0.07166016, -0.2147138, 0.09378708, 0.24550334, -0.0834075, -0.033147786, -0.022304727, -0.31062204, 0.027651973, 0.109098755, 0.18889032, 0.1163026, 0.13863255) * go_0(1.0, 0.0);\n    result += mat4(0.15266588, -0.14901319, 0.033916786, 0.09381096, -0.08196443, -0.16194504, 0.035789456, 0.21234898, -0.48724765, 0.2619442, -0.11215393, 0.25061038, 0.022344576, 0.0116525125, 0.111661114, -0.15242295) * go_0(1.0, 1.0);\n    result += mat4(0.020475458, 0.0797404, -0.13576819, 0.009681671, 0.030504882, 0.049232908, 0.022025917, 0.16912088, -0.23914136, -0.084663324, 0.020925451, -0.1023938, 0.035916872, -0.07538111, -0.11470242, 0.15238516) * go_1(-1.0, -1.0);\n    result += mat4(-0.12941381, 0.08509899, -0.029489802, -0.09148447, -0.089406274, -0.116145454, -0.08979843, 0.11908148, 0.15473351, -0.21687616, 0.12607013, -0.08244334, -0.079580925, -0.16613089, -0.09287793, -0.03412643) * go_1(-1.0, 0.0);\n    result += mat4(-0.023578499, 0.07394217, -0.13069086, -0.1060499, -0.07559958, -0.21839201, 0.1090753, 0.0787872, 0.07677037, -0.25998843, 0.20039314, 0.046882212, 0.31871012, -0.3048051, 0.15118991, -0.00518087) * go_1(-1.0, 1.0);\n    result += mat4(-0.15338503, -0.11057532, 0.075839415, -0.18592294, -0.0155324, 0.038140323, -0.10498194, 0.09070477, 0.05108992, -0.047939524, -0.091004305, 0.09649005, -0.10967152, -0.051909525, -0.05314551, 0.09661584) * go_1(0.0, -1.0);\n    result += mat4(-0.14458802, -0.053263694, -0.0010885567, 0.23342133, 0.01918937, 0.12026143, -0.15691495, 0.30480555, -0.08725869, 0.19082253, 0.3594973, 0.016653897, 0.045152336, -0.088590585, 0.0069655925, 0.1392425) * go_1(0.0, 0.0);\n    result += mat4(0.17944881, -0.17950764, 0.13282645, 0.030974053, 0.32233685, 0.18067117, -0.11472813, 0.097301506, -0.047649745, -0.1053861, -0.081039384, 0.035132434, 0.10204545, 0.085582554, -0.13153993, -0.021741152) * go_1(0.0, 1.0);\n    result += mat4(-0.15573682, 0.16409989, -0.22574787, -0.03877603, -0.18285516, 0.11638645, 0.18321282, -0.017770218, 0.18230622, 0.16433364, -0.12795393, -0.03805153, 0.14386104, -0.0891527, -0.056928284, -0.10961495) * go_1(1.0, -1.0);\n    result += mat4(0.257622, 0.052519716, -0.25421762, -0.1887382, -0.083568096, -0.0064690276, -0.029110614, 0.103327505, -0.17006217, 0.2254096, -0.29366904, 0.04302887, -0.10198446, -0.24423616, 0.16781262, -0.005019004) * go_1(1.0, 0.0);\n    result += mat4(0.103393994, -0.059044626, -0.18192382, 0.0990813, -0.26143607, 0.11036474, 0.04788275, -0.096738026, 0.12825653, 0.13631694, -0.077904984, -0.020790676, -0.25118098, 0.122588515, -0.049440473, -0.10758222) * go_1(1.0, 1.0);\n    result += mat4(0.06693113, -0.13647175, 0.131139, 0.13143918, 0.081720434, 0.117537096, 0.15387627, -0.008771362, 0.08513583, 0.023794742, -0.0661625, 0.115793936, 0.0023350024, 0.02215075, -0.0494433, -0.013404977) * go_2(-1.0, -1.0);\n    result += mat4(0.041419264, -0.17622781, 0.028418267, 0.12114493, -0.23587078, 0.08457395, 0.014364018, -0.103271864, -0.051572207, -0.026424447, 0.16755055, -0.10763651, -0.033440586, 0.068594255, -0.050668504, 0.1941505) * go_2(-1.0, 0.0);\n    result += mat4(-0.2780181, 0.037816502, -0.11516711, -0.09822884, 0.13762361, -0.14317706, 0.14350282, 0.000623895, -0.08601606, 0.08118504, 0.15497385, -0.04721711, -0.008936935, -0.014223618, -0.09641698, -0.013884213) * go_2(-1.0, 1.0);\n    result += mat4(0.14349665, -0.03144472, -0.057813704, 0.0667044, 0.09026094, 0.051366236, 0.11139983, -0.015782114, -0.18314016, -0.18774192, 0.0014838242, 0.15759028, 0.062388215, 0.13626057, 0.02576217, -0.06317815) * go_2(0.0, -1.0);\n    result += mat4(0.07151769, 0.14508991, 0.1736844, -0.11487795, -0.07999805, -0.07797908, 0.037923355, -0.059138823, -0.23531209, -0.040207293, -0.068355694, -0.024296658, -0.114820175, 0.19726487, 0.21772414, 0.03659222) * go_2(0.0, 0.0);\n    result += mat4(0.16858695, -0.12135113, 0.009391182, -0.081519485, 0.13340487, 0.07007004, 0.094124354, 0.035519842, -0.3320139, -0.06624027, -0.14716229, -0.09205287, 0.12664132, -0.05655441, 0.0123263765, 0.04641279) * go_2(0.0, 1.0);\n    result += mat4(0.19018422, -0.15428329, -0.009354114, 0.04165953, 0.11024837, -0.107493006, -0.05807292, -0.048029456, 0.24319384, -0.10542357, -0.013699952, 0.06228662, -0.06808749, -0.023227982, 0.16528323, -0.05610251) * go_2(1.0, -1.0);\n    result += mat4(-0.008616222, 0.077674195, -0.08638503, 0.09293109, 0.072474636, 0.05004233, -0.20591061, -0.005301386, -0.15486047, 0.15038474, 0.1262478, 0.021724822, 0.02274613, -0.3088281, -0.08437887, -0.10684698) * go_2(1.0, 0.0);\n    result += mat4(-0.16960032, 0.09365251, -0.030414175, -0.010766254, 0.18181023, 0.12130318, 0.08913089, -0.06070321, 0.05200306, 0.092584535, 0.17694671, 0.033796314, -0.038107123, -0.04335955, -0.049443472, 0.30465958) * go_2(1.0, 1.0);\n    result += mat4(0.07661484, -0.009945252, 0.12866217, -0.07592757, -0.21030053, 0.014371748, -0.072458774, -0.04700072, 0.15534303, 0.2007125, -0.15699059, -0.032897495, 0.08110436, -0.11243608, 0.008632577, -0.10153441) * go_3(-1.0, -1.0);\n    result += mat4(-0.034697928, 0.06928288, -0.2796273, 0.14405379, 0.12248569, 0.036539096, 0.06607706, 0.077684596, -0.16473202, 0.1665916, -0.29977503, 0.21047153, 0.13114224, -0.091579035, -0.045458574, 0.03254245) * go_3(-1.0, 0.0);\n    result += mat4(0.053284872, 0.053366095, -0.26152626, -0.03123967, -0.031794485, 0.17670582, -0.07450994, 0.017521491, -0.040290453, 0.38342363, -0.25021288, -0.014660264, 0.1621895, 0.25041878, -0.12124821, 0.068036206) * go_3(-1.0, 1.0);\n    result += mat4(0.11366693, -0.030863572, -0.07411263, 0.12475283, -0.046070684, -0.09033321, 0.013222701, 0.06798592, -0.32814804, 0.057653826, -0.14082801, -0.00217398, -0.22856179, -0.19058353, -0.20992154, -0.03701372) * go_3(0.0, -1.0);\n    result += mat4(0.20345633, -0.1332355, 0.27152926, -0.13477845, -0.25242096, -0.28281286, 0.31289554, 0.14284514, 0.53362453, -0.46766588, 0.4518293, -0.39291728, -0.3573227, -0.014670052, 0.0051881406, 0.16552156) * go_3(0.0, 0.0);\n    result += mat4(-0.15017267, -0.07792945, -0.204405, 0.13964304, -0.13642666, -0.10228306, 0.03238279, -0.08689329, -0.072262034, -0.0258388, 0.05689183, 0.055701543, -0.19800112, 0.012217054, -0.033292748, -0.047611095) * go_3(0.0, 1.0);\n    result += mat4(-0.014704416, -0.12203891, 0.066083655, -0.1409769, 0.0041513643, -0.087383606, -0.17498164, 0.11327789, -0.25947225, -0.0016027623, 0.08202566, 0.042270098, 0.006429511, -0.26576808, -0.08461341, 0.049376782) * go_3(1.0, -1.0);\n    result += mat4(0.0695189, -0.14753938, 0.09578246, -0.16607563, -0.0105561055, 0.17166016, 0.027422488, -0.14175262, -0.009492696, -0.23449713, 0.018270867, 0.14635146, 0.33451268, 0.030959005, -0.46468422, 0.024256868) * go_3(1.0, 0.0);\n    result += mat4(-0.16865666, -0.00015881563, -0.054488145, -0.06222717, -0.032101758, 0.06485387, -0.0028512608, 0.046645947, 0.017593225, -0.19447896, -0.024742266, 0.03970127, 0.29845607, -0.16168733, 0.035172883, 0.07924657) * go_3(1.0, 1.0);\n    result += vec4(0.103826486, 0.045373913, 0.11565896, -0.06568643);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!SAVE conv2d_3_tf\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.1851775, 0.053705044, 0.033816848, -0.018555025, -0.21204336, -0.01706974, 0.088259794, -0.13126148, 0.10729598, -0.043457437, 0.08634712, 0.09220895, 0.062131613, -0.01995871, 0.05181067, 0.18520063) * go_0(-1.0, -1.0);\n    result += mat4(0.1662002, -0.14197104, -0.052809287, 0.025287712, -0.08330898, -0.08998097, -0.15642618, -0.14941245, -0.03481203, 0.061857622, 0.26051775, -0.0005498248, 0.086427025, 0.024108192, -0.12418039, 0.022286376) * go_0(-1.0, 0.0);\n    result += mat4(0.058200672, -0.3073398, 0.17150162, -0.13394679, -0.075118184, -0.14607768, -0.006172172, 0.007731589, -0.21818224, -0.06449433, -0.038958784, 0.037722416, 0.28699976, -0.027563032, 0.23295315, 0.028444216) * go_0(-1.0, 1.0);\n    result += mat4(0.12871371, 0.0064904913, 0.14985761, -0.10923005, 0.17413563, 0.1599109, -0.08457703, 0.108153716, -0.08871187, -0.06661137, 0.2754416, -0.009667768, 0.39819396, 0.12392097, 0.14145902, 0.0019376524) * go_0(0.0, -1.0);\n    result += mat4(0.13893189, 0.12715353, 0.015191678, -0.21003054, -0.030412354, -0.01676613, -0.19799289, -0.006130075, 0.37676954, -0.14475077, -0.2065198, -0.30432892, -0.14944535, -0.09121536, -0.107600585, -0.24462196) * go_0(0.0, 0.0);\n    result += mat4(-0.11653076, -0.0068671284, -0.02249137, -0.17877012, -0.15063138, -0.13514869, 0.107643366, -0.03196477, -0.086422764, 0.3079287, 0.17584166, -0.032449376, -0.06917114, -0.2682637, -0.18978168, -0.037039287) * go_0(0.0, 1.0);\n    result += mat4(0.12014731, -0.030360512, -0.12954475, -0.110275604, -0.077214256, 0.019689744, 0.22149551, -0.002266716, 0.09697784, -0.124532826, -0.16776511, -0.034212478, -0.36935154, 0.016926935, 0.1363609, 0.20415346) * go_0(1.0, -1.0);\n    result += mat4(-0.11199535, -0.001692563, -0.09058429, -0.08437503, 0.092625685, 0.06046257, 0.25509837, -0.011657033, -0.17949764, -0.10718947, -0.1180669, -0.24681842, -0.1747311, 0.0014518246, -0.042863015, 0.06103357) * go_0(1.0, 0.0);\n    result += mat4(0.14979295, -0.037154514, 0.01957725, 0.012282435, 0.09168596, -0.05552286, 0.111671515, 0.0078630615, -0.10319766, -0.06416261, -0.23097566, -0.13931875, 0.2110811, 0.013095802, -0.2306504, -0.025639111) * go_0(1.0, 1.0);\n    result += mat4(-0.10091975, -0.10095426, -0.023449723, -0.022170888, 0.054953706, -0.13049407, 0.08289061, 0.023241632, 0.08735388, -0.0058387457, 0.17897247, 0.011434436, 0.008181139, -0.0034718404, -0.015372735, -0.07657766) * go_1(-1.0, -1.0);\n    result += mat4(-0.023442164, 0.07535702, 0.024391165, -0.050532013, 0.044168636, 0.0062343236, -0.019756999, -0.009695123, 0.10102337, 0.0052776975, -0.14944167, -0.060957722, 0.24367364, -0.08069369, 0.12170072, -0.047048368) * go_1(-1.0, 0.0);\n    result += mat4(-0.18376935, -0.08407229, -0.12943378, 0.0738419, -0.12404976, -0.13367929, 0.11265896, -0.021353, 0.003783386, 0.50088304, 0.14058582, 0.041053623, 0.038247623, -0.014179976, 0.007905778, -0.042492237) * go_1(-1.0, 1.0);\n    result += mat4(-0.046272535, 0.052449115, 0.17190954, -0.004745371, -0.045572635, -0.09292636, 0.36309823, 0.16673928, -0.099154025, -0.109614775, 0.17803112, 0.19907133, -0.14306267, 0.06898593, 0.11493454, 0.06795014) * go_1(0.0, -1.0);\n    result += mat4(0.26181114, -0.044014625, -0.21605036, -0.08646438, 0.21038742, -0.084986, 0.0504626, 0.17514943, -0.25218952, -0.18691514, 0.057650108, 0.08653614, -0.101205684, 0.03176334, 0.18569492, 0.17973189) * go_1(0.0, 0.0);\n    result += mat4(-0.0339215, 0.20112811, -0.12986277, 0.028961731, -0.056813832, 0.04451147, -0.07827432, -0.0860976, 0.096853435, 0.3483546, -0.35758162, -0.11749375, -0.035918653, 0.06140711, -0.08520154, 0.02418808) * go_1(0.0, 1.0);\n    result += mat4(-0.09643022, -0.10491069, 0.0068604187, 0.023679713, 0.096521445, -0.29323488, 0.33353668, 0.112864286, -0.1172182, -0.07233183, 0.06607239, 0.08589609, 0.055790007, 0.14396138, -0.14191268, 0.00034840964) * go_1(1.0, -1.0);\n    result += mat4(0.15357164, -0.038462736, 0.08143956, 0.1744909, 0.40503287, -0.114508316, 0.003937322, 0.2536635, -0.042445306, -0.15622465, 0.09155284, 0.010992155, -0.20646071, 0.022801135, 0.08894491, 0.069300614) * go_1(1.0, 0.0);\n    result += mat4(-0.12663515, 0.023849454, -0.053604446, 0.12082873, -0.247968, -0.020969635, -0.03831894, -0.014617553, 0.22630337, 0.037801865, 0.052950703, 0.04285706, -0.14487264, 0.20786528, -0.08719664, 0.1752347) * go_1(1.0, 1.0);\n    result += mat4(-0.073527604, -0.050752833, 0.051830504, 0.32868716, 0.17474994, 0.016937364, -0.08792601, -0.024481766, -0.022229593, 0.030706186, 0.09213566, -0.076506205, 0.073404044, 0.10368055, -0.175889, -0.08453031) * go_2(-1.0, -1.0);\n    result += mat4(-0.06838216, 0.007698341, 0.063972116, -0.015604406, 0.16135305, 0.18044342, 0.024137018, -0.23326185, 0.13235588, -0.009096587, -0.058368143, -0.077040404, 0.0011419816, -0.09246194, 0.061036937, 0.049564146) * go_2(-1.0, 0.0);\n    result += mat4(0.023225296, -0.00060856267, -0.07775185, 0.016958566, -0.2641349, -0.08263046, -0.15350416, -0.30203494, 0.113956556, -0.010813236, -0.017738314, -0.13689043, -0.10318342, 0.025793184, -0.010336172, 0.09733422) * go_2(-1.0, 1.0);\n    result += mat4(-0.04462596, 0.052866418, -0.34754288, 0.05540498, -0.24492586, -0.32016864, 0.18145293, 0.24873725, 0.32388234, -0.034801524, -0.1347588, -0.07565546, 0.015183539, 0.05059595, 0.08090056, 0.05930932) * go_2(0.0, -1.0);\n    result += mat4(0.045346696, -0.052527856, 0.052270077, 0.13417454, 0.05200045, 0.028119288, 0.005115497, 0.22952151, -0.2158375, 0.12241308, 0.3507457, 0.08616576, 0.07592416, 0.28470486, 0.3432788, 0.24857087) * go_2(0.0, 0.0);\n    result += mat4(0.21311626, 0.052607164, 0.1248861, 0.20193806, 0.045226507, 0.14512901, -0.15103437, -0.17926466, 0.11657411, -0.32711068, -0.16332194, -0.07793982, -0.21802668, 0.5183869, -0.13567342, 0.07823041) * go_2(0.0, 1.0);\n    result += mat4(0.00796368, 0.048073012, -0.14537893, -0.021708772, 0.036246423, 0.1062395, 0.12605369, 0.007073524, -0.1572743, 0.07439501, 0.089162275, -0.0039608316, 0.332032, -0.05461242, -0.17615359, -0.10240517) * go_2(1.0, -1.0);\n    result += mat4(0.20636982, -0.0024615112, -0.10625786, 0.024270926, 0.061810836, -0.13585201, -0.16581286, 0.23549418, 0.01928842, 0.07404979, -0.054449487, 0.04096373, 0.046939734, 0.003980803, 0.02111498, 0.064925276) * go_2(1.0, 0.0);\n    result += mat4(0.10485388, 0.06850885, -0.11292169, 0.16991565, -0.15282536, 0.124175504, -0.050431166, -0.06689582, -0.00059811946, 0.033696912, 0.11055047, 0.033060126, -0.17472714, 0.0048819613, -0.04478706, -0.1344572) * go_2(1.0, 1.0);\n    result += mat4(-0.20473132, 0.056477875, 0.059559986, 0.115130566, -0.058425788, -0.035971727, 0.08334707, -0.096510135, -0.23206294, 0.10635798, -0.21575621, -0.07063254, 0.03877511, -0.107549034, 0.22248401, 0.21702304) * go_3(-1.0, -1.0);\n    result += mat4(-0.02557767, 0.09886609, -0.100499466, 0.16687396, -0.084830604, 0.03150401, -0.049512494, 0.05595696, -0.13193256, -0.08585273, 0.14247662, 0.12290477, -0.07168309, 0.14531752, -0.048359327, 0.27716598) * go_3(-1.0, 0.0);\n    result += mat4(0.13297586, 0.20674329, 0.14469388, 0.08981846, -0.004231366, -0.02819193, 0.15470329, 0.17299837, 0.113062344, -0.22716297, -0.21754944, -0.00083956274, -0.14160508, 0.1808253, 0.11268379, 0.27335623) * go_3(-1.0, 1.0);\n    result += mat4(0.07497518, -0.06799594, -0.018158078, -0.00038999433, -0.15169668, -0.06928238, -0.33672288, -0.105485775, 0.33106267, 0.06698315, 0.019718744, -0.06810211, -0.35186404, -0.29145968, -0.056863394, 0.21498048) * go_3(0.0, -1.0);\n    result += mat4(-0.013215512, -0.24763754, 0.20965266, 0.1068435, -0.13234195, 0.053566497, 0.05061848, -0.28645232, 0.15518288, 0.23247199, 0.017553907, -0.25181335, -0.048030723, -0.06663929, -0.111026704, -0.12663394) * go_3(0.0, 0.0);\n    result += mat4(-0.010501938, -0.17995767, 0.06010859, 0.050185587, 0.108627126, -0.101203434, 0.07558728, 0.060466755, -0.106942676, -0.35854608, 0.16015992, 0.16823332, -0.06543775, -0.37310675, 0.014043972, -0.18328045) * go_3(0.0, 1.0);\n    result += mat4(0.09712849, 0.013983463, 0.07291423, 0.031715546, 0.030862397, 0.045510456, -0.22066842, 0.063464865, 0.11721659, -0.10596602, -0.20611264, 0.052158818, -0.3961766, -0.03781582, 0.17633812, 0.1316111) * go_3(1.0, -1.0);\n    result += mat4(-0.25029674, 0.07153423, -0.35125682, -0.18255402, -0.19569087, 0.00432772, -0.0969035, -0.24648514, -0.0040922165, 0.037500706, -0.038137026, 0.056214277, -0.048258524, 0.03567822, -0.05033007, -0.24696785) * go_3(1.0, 0.0);\n    result += mat4(-0.03465209, -0.012495964, 0.22782089, 0.012034795, 0.2916752, 0.08264436, 0.15387125, -0.1473455, -0.15614432, 0.05536727, -0.027079755, 0.010725311, -0.03325222, -0.089212805, -0.10559839, -0.19647683) * go_3(1.0, 1.0);\n    result += vec4(0.0001705175, -0.031081453, 0.010100773, -0.027214011);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!SAVE conv2d_3_tf1\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.026301445, -0.021575214, 0.22165509, 0.059994068, 0.03341161, 0.1831188, 0.20342293, 0.110160105, 0.03908121, 0.020673111, 0.07239561, 0.038754333, 0.15266368, 0.16526422, 0.062376205, -0.09759537) * go_0(-1.0, -1.0);\n    result += mat4(0.19817191, 0.10267733, 0.17744653, 0.23283184, 0.18810122, 0.2708428, -0.12651879, 0.020756349, 0.039632563, -0.22201295, 0.04873703, 0.09159713, 0.13838065, 0.21169297, 0.30816007, 0.044463675) * go_0(-1.0, 0.0);\n    result += mat4(-0.27859214, 0.07277634, 0.0021458792, 0.0089682285, -0.069680706, 0.090415835, -0.057762265, 0.18703683, -0.03514389, -0.102816254, -0.036509827, 0.038066104, -0.0168311, 0.094478935, 0.04079697, -0.049064912) * go_0(-1.0, 1.0);\n    result += mat4(-0.20913245, -0.110538535, -0.08584027, -0.1222067, 0.05414807, -0.045247085, 0.07351766, -0.002078549, -0.1270987, -0.10164512, -0.1857815, 0.08845066, -0.03743333, -0.098948084, 0.21244387, 0.10441866) * go_0(0.0, -1.0);\n    result += mat4(0.015990427, 0.36396438, -0.24094687, 0.30236533, -0.13271736, 0.06057376, -0.19678196, -0.28577125, -0.25427434, -0.08400598, 0.07284403, -0.18552442, -0.16425897, 0.097259276, -0.32386774, -0.2190484) * go_0(0.0, 0.0);\n    result += mat4(-0.004581924, -0.13954072, -0.122360416, 0.14132866, -0.08529257, -0.013296556, 0.0848472, 0.09336581, 0.10332182, -0.016313016, 0.07103558, 0.032564916, -0.13478759, -0.20207484, 0.12986964, 0.1219679) * go_0(0.0, 1.0);\n    result += mat4(0.09817874, -0.10573357, 0.100535244, 0.19608764, -0.13303067, 0.024192972, -0.030689823, 0.02574889, 0.051233094, 0.03489235, -0.18465245, -0.06943822, -0.031604882, 0.1519888, 0.09348508, 0.09187296) * go_0(1.0, -1.0);\n    result += mat4(-0.21365458, -0.23696984, 0.13097638, -0.09435498, 0.16467983, -0.066370346, 0.1269104, -0.095128186, 0.09954892, 0.12489504, -0.43418056, 0.106512725, -0.17860703, -0.07114084, -0.07630834, -0.26642478) * go_0(1.0, 0.0);\n    result += mat4(-0.009044342, 0.02711196, -0.14873673, 0.015405045, 0.0071443473, -0.025285944, 0.07409282, 0.06338527, 0.0149676185, 0.011741382, -0.2133069, -0.028912885, 0.19420496, 0.039629057, 0.057636812, 0.15214856) * go_0(1.0, 1.0);\n    result += mat4(0.07629928, 0.25540486, -0.050925937, -0.18136702, 0.02261603, 0.22343902, 0.003270321, 0.10735731, -0.12541203, -0.10208828, 0.012832783, 0.2591262, 0.08122926, -0.009837677, 0.10308358, 0.19236866) * go_1(-1.0, -1.0);\n    result += mat4(0.0896358, 0.27571487, 0.04406029, -0.047453407, -0.08587119, 0.16366854, 0.20622262, 0.08347545, -0.3501584, -0.28434548, -0.07592983, 0.09098784, 0.07605388, 0.09677056, 0.0015295541, 0.05102585) * go_1(-1.0, 0.0);\n    result += mat4(0.18255898, 0.18618028, 0.0017002645, -0.013004655, -0.06436534, 0.13967068, 0.063077755, -0.10632303, -0.20803222, -0.028537111, -0.03144366, -0.08555215, 0.05154303, 0.02431626, 0.15246728, -0.013708507) * go_1(-1.0, 1.0);\n    result += mat4(-0.020998938, -0.05026291, 0.03700117, 0.00830308, -0.1949294, 0.0026698054, -0.034649856, 0.19784226, -0.083901435, -0.069783084, -0.1504053, 0.16595264, -0.07480141, 0.16067508, 0.06010996, -0.021359695) * go_1(0.0, -1.0);\n    result += mat4(-0.040828142, -0.20158486, 0.034770954, -0.1894161, 0.11665004, 0.29729164, -0.10584386, 0.13165873, -0.18863006, -0.26719162, -0.047613148, -0.12728356, -0.2033613, 0.10550052, 0.20095508, -0.11275811) * go_1(0.0, 0.0);\n    result += mat4(-0.0785033, -0.1896073, -0.051492307, -0.1694358, 0.1368308, 0.049355216, -0.05707422, 0.079159185, 0.024578957, -0.0923136, 0.089215435, 0.28670043, 0.027932687, 0.06510816, 0.10810999, 0.05990052) * go_1(0.0, 1.0);\n    result += mat4(0.08135192, 0.0001326522, -0.16098668, -0.18663193, -0.10280192, 0.078255914, 0.047648013, 0.08326376, 0.055962667, 0.06302574, -0.080121025, -0.031820554, -0.019117938, 0.12515336, 0.09794088, -0.03276838) * go_1(1.0, -1.0);\n    result += mat4(0.280923, 0.24079335, 0.007883573, 0.06270414, 0.3055441, 0.19291803, -0.16041607, 0.14836526, 0.0013885222, 0.04538063, 0.10742898, -0.064491205, 0.048174977, 4.237692e-05, -0.15194727, 0.024381457) * go_1(1.0, 0.0);\n    result += mat4(-0.0009164131, -0.031949926, 0.0076425644, -0.036870714, -0.0031292974, 0.017726978, -0.20172147, -0.0770472, 0.26379177, 0.108997814, 0.08069395, 0.2126177, 0.012075376, -0.029457828, 0.062730506, -0.15754452) * go_1(1.0, 1.0);\n    result += mat4(0.09167904, -0.2657421, -0.03443356, 0.03315832, -0.015365421, -0.1029612, -0.108251, 0.04261033, -0.097120754, -0.05616668, -0.09275983, 0.024902184, 0.050058514, -0.013761632, 0.07555132, -0.0046676896) * go_2(-1.0, -1.0);\n    result += mat4(-0.10743835, -0.0007361781, -0.042085417, -0.08237517, -0.10094376, -0.24007876, 0.13924706, -0.07526801, 0.01158322, 0.15491122, 0.0069442675, -0.004242352, 0.11429785, 0.02994726, -0.11829945, -0.04108612) * go_2(-1.0, 0.0);\n    result += mat4(0.073622055, -0.064717196, -0.0025231615, 0.13256475, 0.20159899, 0.047977835, -0.10289233, -0.18419135, -0.00888952, 0.059428576, -0.053062655, -0.02730631, 0.14545685, -0.08686949, 0.17454128, 0.035443828) * go_2(-1.0, 1.0);\n    result += mat4(-0.010146019, 0.06712568, 0.12614638, 0.023590917, 0.025756737, 0.06603747, -0.17108095, -0.06179699, 0.027241204, -0.13196802, 0.043475866, -0.0397495, 0.05306092, 0.035672903, 0.047219284, -0.16680142) * go_2(0.0, -1.0);\n    result += mat4(0.079427816, -0.06716479, 0.19028603, -0.19694683, -0.061598092, -0.07471188, 0.21170339, 0.30140215, -0.0023369973, 0.04688297, -0.14154115, 0.19283508, 0.1339858, -0.09116279, 0.15305163, 0.029108394) * go_2(0.0, 0.0);\n    result += mat4(-0.14902157, -0.03339153, -0.08532003, -0.10736339, 0.08702709, 0.07607574, -0.09955836, -0.016585784, -0.030078214, -0.060374748, -0.2854279, 0.02441719, 0.034877967, 0.2099041, 0.11125731, -0.059071556) * go_2(0.0, 1.0);\n    result += mat4(-0.08436325, 0.06893047, -0.045362443, -0.02237741, -0.07583875, -0.034830183, -0.024008518, -0.2882329, -0.011109783, 0.101859994, 0.091137715, 0.0020565533, -0.044729806, -0.18168025, 0.069466636, 0.04994174) * go_2(1.0, -1.0);\n    result += mat4(0.11915174, 0.089596465, -0.18965814, 0.015218237, 0.13500094, 0.19921367, -0.008298205, 0.29650384, -0.049439427, -0.27590424, 0.36169067, -0.030582754, 0.02151196, 0.019915426, 0.04543398, 0.16126189) * go_2(1.0, 0.0);\n    result += mat4(0.1620274, -0.08264547, 0.082442135, -0.0034478644, 0.09888509, -0.0034957859, -0.107241705, -0.17729597, -0.05138647, 0.02052103, -0.019507123, 0.037574988, -0.1694345, 0.17871588, -0.22510391, 0.019049853) * go_2(1.0, 1.0);\n    result += mat4(-0.10962245, -0.1329873, -0.060855392, 0.025941676, -0.19536193, -0.120365486, -0.04313703, -0.052912965, 0.20854498, 0.08341353, 0.008687068, -0.20432276, 0.15677948, -0.19000018, 0.01821201, -0.041512605) * go_3(-1.0, -1.0);\n    result += mat4(0.012287526, -0.14180368, -0.098788455, 0.025949089, 0.09442778, 0.2247651, -0.12453263, 0.10435483, 0.274603, 0.06133054, 0.10506106, 0.14727746, -0.048299775, -0.082819685, 0.07319359, -0.047460355) * go_3(-1.0, 0.0);\n    result += mat4(-0.070726536, -0.034744017, 0.07521428, 0.070649154, -0.05958955, -0.100232825, -0.010651838, 0.045392875, 0.2930271, -0.04952355, 0.3112155, 0.117203265, 0.025166962, 0.11176862, 0.06716659, 0.07175864) * go_3(-1.0, 1.0);\n    result += mat4(-0.011560962, -0.14032063, -0.17424704, 0.07652749, -0.04220116, 0.052874275, -0.00225693, -0.031843517, -0.07520102, -0.13775803, 0.2449317, 0.069658786, 0.052280303, -0.105218224, 0.03574522, -0.020500354) * go_3(0.0, -1.0);\n    result += mat4(0.08793712, 0.26712346, 0.08315631, 0.23813692, -0.04439029, 0.031587064, 0.09561177, -0.13380238, -0.24982157, 0.31701845, -0.3875432, 0.10487225, 0.09201869, -0.037252493, -0.006935219, -0.14650282) * go_3(0.0, 0.0);\n    result += mat4(0.077635325, 0.13732299, -0.071563005, 0.096517466, -0.15051986, -0.111744404, 0.03996857, -0.052670125, -0.1819665, 0.054554947, -0.13774712, -0.20061246, -0.0023742192, 0.15647805, -0.024121126, 0.075497724) * go_3(0.0, 1.0);\n    result += mat4(0.0073632775, -0.06535298, 0.039895996, 0.20666869, 0.13625242, 0.04823007, -0.07135618, 0.04787906, 0.01383074, 0.15382123, -0.15519714, 0.056721795, 0.061946746, -0.0586851, 0.028934354, -0.02264129) * go_3(1.0, -1.0);\n    result += mat4(-0.19791882, -0.111910924, -0.010451344, -0.30566537, -0.1416239, -0.14523096, 0.116883226, -0.18241516, 0.2680614, -0.18487626, 0.17472346, 0.08346682, -0.14510359, -0.029229192, -0.005879142, 0.050247498) * go_3(1.0, 0.0);\n    result += mat4(0.030153519, -0.092469186, -0.022912916, 0.10200855, -0.04237032, -0.05917764, 0.10479645, -0.05619482, -0.18949397, -0.019547248, 0.013868889, -0.1524476, 0.14048979, -0.032521486, 0.1322921, 0.070972025) * go_3(1.0, 1.0);\n    result += vec4(0.012053958, -4.6962363e-05, 0.0020099226, -0.033494607);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!SAVE conv2d_4_tf\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.06738501, 0.034009207, -0.21538448, 0.14296548, 0.12896985, -0.23526315, -0.08848608, 0.019602662, 0.14937137, 0.11353096, 0.11884168, -0.016765572, 0.030985225, 0.046430565, 0.06614828, -0.19202724) * go_0(-1.0, -1.0);\n    result += mat4(-0.10326068, 0.11014975, 0.17069744, -0.21474148, 0.16761585, 0.13434832, -0.101021074, 0.006307025, 0.07478008, -0.1060066, 0.035315692, 0.033488914, -0.24906659, 0.06269967, 0.11120735, -0.040928528) * go_0(-1.0, 0.0);\n    result += mat4(0.09334615, 0.057705753, 0.12213245, -0.06402275, 0.30694544, 0.034585163, 0.20345578, 0.07489286, 0.07483618, -0.14240396, 0.034846418, -0.03811241, 0.010882573, 0.13204294, 0.017563924, -0.047203008) * go_0(-1.0, 1.0);\n    result += mat4(-0.21673942, -0.024010994, -0.10238504, -0.041160326, 0.06838163, -0.20950818, 0.06526309, -0.079094924, 0.02208821, -0.28130978, 0.086275116, -0.089067616, 0.12133826, -0.062600106, -0.020521903, -0.07654401) * go_0(0.0, -1.0);\n    result += mat4(-0.03055029, -0.15683146, -0.20331301, -0.06252028, 0.13350682, 0.20338707, 0.038425338, 0.1581342, -0.27322498, -0.14999662, -0.16681097, 0.0971585, -0.20014858, -0.081635274, -0.0781877, -0.20625232) * go_0(0.0, 0.0);\n    result += mat4(0.38375977, -0.019825654, 0.1886721, 0.22616312, 0.3402173, 0.1825304, -0.05531195, 0.30973226, -0.2676023, 0.14413352, 0.021706983, 0.01732799, 0.23466855, -0.13805965, 0.22570935, 0.018103868) * go_0(0.0, 1.0);\n    result += mat4(-0.15169825, 0.0270689, -0.2503316, 0.17289825, -0.16437647, 0.039233048, -0.35572487, -0.048393793, 0.19270042, 0.24260359, 0.12041881, -0.0009793913, 0.11656858, 0.11007414, -0.0757491, 0.047933612) * go_0(1.0, -1.0);\n    result += mat4(-0.18657999, -0.11252566, -0.05237504, -0.07368097, 0.13882741, -0.13710637, -0.006996468, -0.062354874, 0.23452504, 0.15333645, -0.0022776406, -0.17910439, 0.03629509, -0.16264829, -0.010011833, -0.15313338) * go_0(1.0, 0.0);\n    result += mat4(-0.060544558, -0.04913478, -0.061717357, 0.02323648, 0.28739056, -0.07434013, 0.19110644, 0.100050166, 0.0073363045, 0.08185653, -0.024797903, -0.14424153, -0.20838726, 0.16154376, -0.048517212, -0.025453888) * go_0(1.0, 1.0);\n    result += mat4(0.14975396, -0.13142908, 0.36210674, -0.054021083, -0.10632155, 0.045697935, -0.18946633, 0.02228141, -0.08919603, 0.09800842, -0.17634438, 0.09512711, -0.03425503, -0.12298555, -0.05354435, -0.17112055) * go_1(-1.0, -1.0);\n    result += mat4(0.09958265, -0.057276618, -0.16262266, -0.06415915, 0.14579074, -0.36784375, 0.08034197, -0.04537706, 0.005460582, 0.22313322, 0.07382161, 0.014990379, 0.044636846, -0.2811128, -0.22621547, -0.06044004) * go_1(-1.0, 0.0);\n    result += mat4(0.10569276, -0.03738662, 0.16100396, 0.058593616, -0.048862137, -0.08796426, 0.20101094, -0.11039573, 0.17196764, -0.04601554, 0.008571281, -0.073729075, 0.051433694, -0.051276565, 0.087334655, -0.0360379) * go_1(-1.0, 1.0);\n    result += mat4(0.011119538, -0.28781965, 0.28637868, -0.1742508, -0.07121849, 0.10379717, 0.012615981, -0.029563965, -0.18678424, 0.05291095, 0.039143506, -0.028248642, -0.014103922, 0.029155696, 0.10433492, 0.16305852) * go_1(0.0, -1.0);\n    result += mat4(-0.2231037, -0.13697462, -0.29124337, 0.08519773, 0.15893684, -0.17763218, 0.06950923, 0.34361118, -0.024844287, 0.044008408, -0.033844844, -0.086971916, -0.07884748, 0.2543499, 0.056884114, 0.10068364) * go_1(0.0, 0.0);\n    result += mat4(-0.07710048, -0.23218372, 0.04346047, 0.21769643, 0.06473219, -0.18066105, -0.2511205, 0.15309611, 0.04535977, 0.16450433, 0.10846344, 0.0016952346, -0.010874939, 0.28966382, -0.121990964, 0.12956186) * go_1(0.0, 1.0);\n    result += mat4(-0.007910202, 0.17766511, 0.14364475, 0.1016258, 0.0051045395, 0.18691733, 0.005813767, -0.0070582186, 0.019418601, -0.1604435, 0.016088275, -0.18265302, -0.15719391, -0.17369832, -0.036745597, -0.19647408) * go_1(1.0, -1.0);\n    result += mat4(0.08938396, -0.0073808245, 0.11225727, -0.012303106, 0.096785046, 0.030483445, 0.027719889, -0.052584838, -0.14887555, -0.03422243, 0.12646855, -0.1722482, 0.010239037, 0.06406088, -0.20053658, 0.01964698) * go_1(1.0, 0.0);\n    result += mat4(-0.120734036, -0.12450362, -0.06582111, 0.1639675, -0.19787048, -0.08049789, -0.014257596, 0.058436662, -0.0009387449, -0.08698089, -0.017400503, 0.06295286, 0.09890349, -0.057190523, -0.103520766, -0.04207548) * go_1(1.0, 1.0);\n    result += mat4(-0.0118413875, -0.031288836, 0.09749554, -0.012266401, -0.07998591, 0.22615653, -0.06207416, 0.03257896, -0.076378696, -0.079426095, -0.13968349, -0.15423697, -0.1091681, -0.02893125, -0.032659534, -0.063735925) * go_2(-1.0, -1.0);\n    result += mat4(0.119372696, 0.013176554, -0.029381052, 0.21919228, 0.045041792, 0.24844484, 0.26363325, 0.08480674, 0.087083444, 0.11984778, -0.088715754, 0.06421046, 0.05225977, -0.05140334, -0.055052705, -0.049854077) * go_2(-1.0, 0.0);\n    result += mat4(0.0035781674, 0.0861361, -0.07675145, -0.056479637, 0.16973391, -0.12113791, 0.10729832, -0.03773517, 0.058618728, 0.12148276, 0.17260705, -0.06968724, 0.076358154, -0.15307103, 0.17700425, -0.13467014) * go_2(-1.0, 1.0);\n    result += mat4(-0.02752418, -0.06366472, -0.025610954, 0.0013539721, -0.06465272, 0.0806373, -0.07336035, 0.10114861, 0.0041146413, 0.15878421, -0.044668555, -0.12150811, -0.1071482, -0.05086587, 0.18589285, 0.05065092) * go_2(0.0, -1.0);\n    result += mat4(0.07200056, 0.021739854, 0.29476613, -0.08475931, 0.15018553, -0.07886365, 0.36336347, -0.020576432, 0.25866082, -0.059272554, 0.054249667, -0.17822553, 0.1755872, 0.3244387, -0.39173844, 0.33894604) * go_2(0.0, 0.0);\n    result += mat4(-0.11570926, 0.1342677, -0.19511898, 0.0075454637, -0.01890476, -0.14239742, 0.18921931, 0.033990458, 0.31306365, -0.006998358, 0.029190077, -0.005679954, -0.15341778, 0.07766778, -0.25691047, -0.0964161) * go_2(0.0, 1.0);\n    result += mat4(0.019746238, 0.0021332854, -0.00879096, -0.1338671, -0.0001600663, -0.29465106, 0.0867611, -0.114963025, 0.07874301, -0.012734178, -0.11124061, -0.010926616, -0.04941506, -0.07516841, 0.116663, -0.29018974) * go_2(1.0, -1.0);\n    result += mat4(-0.01651721, 0.05955898, 0.023618208, 0.098695934, 0.018553663, -0.054378513, 0.1436929, 0.1693743, -0.27483663, 0.029127488, 0.09619316, -0.06109113, -0.08619361, 0.09315214, -0.02478657, 0.18544984) * go_2(1.0, 0.0);\n    result += mat4(0.09570196, -0.016528936, -0.1559397, 0.14312246, 0.04029428, 0.08773151, -0.043646842, 0.17894371, -0.082413055, 0.0027082344, -0.100171275, 0.01547501, 0.18122818, -0.11933676, 0.26404107, -0.3169703) * go_2(1.0, 1.0);\n    result += mat4(-0.12073344, 0.08683522, -0.09249099, 0.058786053, -0.14480567, -0.121013954, 0.033335857, 0.009353379, -0.055087596, -0.13002734, 0.08890566, 0.05508963, -0.0075715426, -0.15936922, -0.03968994, -0.1690259) * go_3(-1.0, -1.0);\n    result += mat4(0.2011206, 0.23898427, 0.23656492, 0.1287573, 0.14850396, 0.40532517, -0.107408255, 0.40119782, 0.099813245, -0.03830304, 0.101520434, -0.026478073, -0.048469637, 0.106440455, 0.056632314, -0.17825997) * go_3(-1.0, 0.0);\n    result += mat4(-0.076735444, 0.05965795, -0.0052469415, -0.21785147, 0.11887833, 0.067560315, 0.051149055, 0.23626682, -0.1297049, -0.035512198, 0.20352256, -0.025064934, 0.04958706, 0.0454198, 0.0113334535, 0.0417486) * go_3(-1.0, 1.0);\n    result += mat4(-0.09055751, 0.033915352, -0.21836667, 0.22006813, -0.099022895, 0.11720966, -0.15686816, -0.13586599, -0.094427735, -0.08831514, -0.06182928, 0.09213704, -0.03642064, 0.18129414, -0.012926811, 0.12179882) * go_3(0.0, -1.0);\n    result += mat4(0.19389409, 0.09512252, 0.14768016, -0.16623649, -0.031052284, -0.026814984, 0.106168024, -0.2026781, -0.04581419, -0.0016849053, -0.04101923, 0.038959503, -0.011938445, 0.20096186, -0.26666564, 0.4824324) * go_3(0.0, 0.0);\n    result += mat4(0.17727576, 0.07309147, 0.12131863, -0.163096, 0.17225246, 0.26256254, 0.27685758, 0.09094053, 0.029605515, -0.20217367, 0.047564875, 0.043115832, 0.15089568, -0.09670934, 0.24131384, 0.03337442) * go_3(0.0, 1.0);\n    result += mat4(-0.34192136, 0.12063195, -0.31159517, 0.04170889, -0.30147067, -0.21330686, -0.1514457, -0.121126845, 0.04409098, 9.2206596e-05, 0.027680017, 0.03230512, -0.27993527, -0.093485355, 0.07568645, -0.23585452) * go_3(1.0, -1.0);\n    result += mat4(0.0537712, -0.20847629, 0.1740093, -0.013894753, -0.32719997, -0.059484575, -0.006098233, -0.10336451, -0.14706188, -0.07424865, -0.07045905, 0.17093194, -0.22147557, 0.09086218, -0.11033544, -0.05306482) * go_3(1.0, 0.0);\n    result += mat4(0.00489003, -0.11509064, -0.021005848, 0.16637677, -0.089347586, 0.17545725, -0.17313693, 0.13742085, -0.14577347, 0.07951095, -0.092139855, 0.017118992, -0.053472433, 0.079414465, 0.0330263, -0.11189824) * go_3(1.0, 1.0);\n    result += vec4(-0.034743138, 0.012946433, -0.082333155, 0.07721756);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!SAVE conv2d_4_tf1\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.25835788, 0.050451655, -0.1845038, -0.07232528, 0.1323318, 0.26276684, 0.10842882, -0.083056524, 0.17426784, -0.3594826, 0.2728965, 0.08388844, -0.004007842, 0.020535901, -0.051425606, 0.07750436) * go_0(-1.0, -1.0);\n    result += mat4(-0.11410436, 0.014572361, -0.27057216, -0.023974562, 0.05234827, 0.15328228, -0.17502303, -0.3199359, 0.12188045, -0.095813684, 0.024145132, 0.0856916, -0.027453909, -0.043129764, 0.16971985, 0.021623038) * go_0(-1.0, 0.0);\n    result += mat4(0.06611095, 0.038625732, -0.13717118, -0.04497733, 0.15213469, 0.04770935, 0.0729271, -0.062052976, 0.004571303, 0.035141192, -0.059409596, 0.044652313, 0.17520894, 0.09665589, -0.1479193, 0.06528058) * go_0(-1.0, 1.0);\n    result += mat4(-0.1845968, 0.091479465, -0.09394898, -0.13545018, -0.029501775, -0.21426639, 0.09255898, 0.1257644, 0.20256902, 0.06267267, 0.10378081, 0.13494423, 0.058310498, 0.03642236, -0.16268995, -0.048100803) * go_0(0.0, -1.0);\n    result += mat4(0.2155119, -0.3683131, 0.049449228, -0.20559964, -0.11761922, -0.2518804, -0.020712897, 0.12895772, -0.07543782, 0.5805017, -0.11301444, -0.038493153, -0.06710986, -0.09321189, 0.108671665, -0.03259695) * go_0(0.0, 0.0);\n    result += mat4(0.035307787, 0.108389005, -0.27493554, 0.27029404, 0.25523573, -0.28636125, -0.20766719, -0.008661457, -0.004480811, -0.046390545, -0.16221444, 0.008979624, -0.061375532, 0.035076566, -0.018924266, 0.01380219) * go_0(0.0, 1.0);\n    result += mat4(-0.051922515, -0.12463486, -0.10383422, 0.02220095, -0.1573033, 0.13980615, 0.13248625, -0.16803266, -0.0692132, -0.21552645, 0.13744529, 0.23034313, 0.0052666534, 0.028977966, 0.07720251, -0.06477756) * go_0(1.0, -1.0);\n    result += mat4(-0.14097473, 0.2770271, -0.172289, -0.03000696, -0.028684044, 0.040578447, -0.2290285, 0.082329154, -0.042402364, -0.20926563, 0.08233207, 0.11862443, -0.07038536, -0.02273004, 0.091550544, -0.065856494) * go_0(1.0, 0.0);\n    result += mat4(0.14879914, -0.023923844, -0.23569296, 0.20306346, 0.17502785, 0.28776234, -0.2788995, 0.10012439, -0.05635638, -0.025840463, 0.09222198, 0.118032, 0.08057015, 0.1286071, 0.060189806, -0.052669708) * go_0(1.0, 1.0);\n    result += mat4(0.07076086, -0.15111323, -0.07427972, 0.008372168, -0.17791592, -0.16254742, 0.013961132, -0.0944912, -0.23380096, 0.17377278, -0.09683394, 0.019931393, -0.12042098, 0.0016406325, 0.09393333, -0.06882231) * go_1(-1.0, -1.0);\n    result += mat4(0.21465093, 0.04142968, 0.06840044, -0.37831602, -0.05549571, 0.044905066, -0.07873589, -0.026804, -0.34764197, 0.022487951, -0.077293746, 0.089457795, -0.110094436, 0.24233972, 0.06285107, -0.10851744) * go_1(-1.0, 0.0);\n    result += mat4(0.093270175, 0.084138945, 0.03938272, 0.063565865, -0.010733802, 0.13554469, -0.06650261, 0.033002816, 0.011187271, -0.12821455, 0.20785914, -0.030438649, -0.124710515, -0.022294303, 0.09732408, 0.057609864) * go_1(-1.0, 1.0);\n    result += mat4(-0.12833868, 0.021577539, -0.02700365, 0.11799592, -0.03655647, -0.04225167, 0.11049353, -0.16036157, 0.049277548, -0.033842396, 0.10020137, 0.095509745, 0.08060231, -0.09237418, -0.035598125, -0.035926737) * go_1(0.0, -1.0);\n    result += mat4(-0.32829186, 0.3492363, 0.030671779, -0.12606762, 0.010437313, 0.2757115, -0.21517593, -0.15800527, -0.12592544, -0.20578934, 0.10444053, 0.12993255, -0.046079267, 0.03834173, -0.19277227, -0.22124454) * go_1(0.0, 0.0);\n    result += mat4(-0.052546192, 0.026082167, 0.13831234, 0.10982424, 0.012946818, -0.12439852, 0.10134106, -0.10050398, -0.04472338, -0.14325236, -0.20579574, 0.0044005127, 0.22013672, -0.32955512, 0.12404084, -0.008160738) * go_1(0.0, 1.0);\n    result += mat4(-0.10774314, -0.31650826, -0.06601711, 0.19635755, -0.12622592, -0.06396423, 0.13856032, 0.16540553, 0.021387719, 0.23377723, -0.053738154, -0.1000186, -0.08338395, -0.052813534, 0.008122962, 0.13732094) * go_1(1.0, -1.0);\n    result += mat4(-0.18270823, 0.06966014, -0.17788303, -0.27303055, -0.077971615, 0.013978423, -0.02039098, 0.12715338, -0.11924171, 0.18900296, -0.085199654, 0.215198, 0.18587974, -0.009749325, 0.0173584, -0.12018259) * go_1(1.0, 0.0);\n    result += mat4(0.052129295, -0.107416354, 0.12711766, 0.03708665, -0.14369462, -0.055359814, -0.16639823, -0.045143317, -0.06925672, -0.040696755, 0.01999809, -0.016040625, -0.02484878, 0.07417094, 0.050875198, 0.2145528) * go_1(1.0, 1.0);\n    result += mat4(0.055696912, -0.16680926, -0.021987487, 0.024941636, -0.0927883, 0.022136632, 0.033782948, -0.10646058, -0.14944647, 0.25457275, 0.046682496, -0.022462368, -0.07886781, 0.08165927, 0.06848105, 0.0063734027) * go_2(-1.0, -1.0);\n    result += mat4(0.037053242, 0.033215813, 0.18291366, 0.12340375, 0.08491059, -0.28442004, -0.0127422465, -0.039834313, -0.23321372, 0.26676926, -0.05636355, -0.15672484, -0.12891728, -0.15486577, -0.032004442, -0.092745155) * go_2(-1.0, 0.0);\n    result += mat4(0.015779478, -0.18457565, 0.24996394, 0.036197674, 0.15694007, 0.15863103, -0.07332398, 0.0016235278, -0.15536517, -0.056062788, 0.14102836, 0.16915025, -0.08001087, 0.07073164, 0.13796777, 0.123867124) * go_2(-1.0, 1.0);\n    result += mat4(0.045792986, -0.15135059, -0.1354885, -0.043678258, -0.35655212, 0.51232076, -0.12816145, -0.046569496, -0.014127674, -0.06282611, -0.098873, -0.06359104, -0.0919222, 0.11822437, 0.079254694, 0.00579688) * go_2(0.0, -1.0);\n    result += mat4(-0.15683417, 0.61610246, -0.3024612, 0.12917964, -0.09303367, 0.23612969, -0.40842506, -0.12374661, -0.07572449, -0.2613284, -0.09970177, -0.015227848, 0.106239066, -0.21411185, 0.051998455, -0.1364518) * go_2(0.0, 0.0);\n    result += mat4(0.23850034, -0.14394449, -0.0031468747, -0.2380617, -0.027200876, -0.041352056, -0.01864445, 0.033848196, -0.12064239, -0.110480845, 0.08450956, -0.22328654, 0.17664163, 0.22268307, 0.050886698, -0.17475672) * go_2(0.0, 1.0);\n    result += mat4(-0.17808256, 0.010803805, 0.03315186, 0.033143792, -0.14205995, 0.25039625, -0.08784382, -0.13454252, 0.19576813, 0.10755282, 0.22821628, 0.019456752, -0.0422955, -0.016182603, -0.12066697, 0.0548465) * go_2(1.0, -1.0);\n    result += mat4(0.11563777, -0.257929, 0.0010403778, 0.080267854, -0.0025255163, 0.2855168, -0.060352214, -0.07816255, -0.00090574916, 0.049510725, 0.03720483, 0.059250016, -0.08674136, 0.20522198, -0.28694284, 0.1299507) * go_2(1.0, 0.0);\n    result += mat4(-0.14638457, 0.04063328, 0.03139636, -0.007934521, 0.07689684, -0.09467145, 0.10607347, 0.054510128, 0.003306194, 0.05347124, 0.062762424, -0.041480847, -0.07677865, -0.139573, 0.010972524, 0.21957156) * go_2(1.0, 1.0);\n    result += mat4(-0.026845628, -0.043439507, 0.034738723, 0.07281683, 0.14474197, 0.031586993, -0.22767854, -0.0707655, 0.105201736, -0.28805482, 0.008668302, -0.16329518, 0.06157049, 0.3803886, 0.26345953, -0.011096537) * go_3(-1.0, -1.0);\n    result += mat4(-0.23328833, 0.085731484, -0.07755016, 0.33559516, 0.07704345, 0.115106605, -0.24114038, -0.44630137, 0.2726737, -0.32170138, -0.009236524, -0.11666051, 0.0457048, 0.07876708, 0.13134004, -0.035318643) * go_3(-1.0, 0.0);\n    result += mat4(-0.05140272, 0.011605703, 0.13899171, -0.05071015, 0.18413687, -0.31413674, -0.13043414, -0.15118152, -0.15326938, -0.10720126, -0.23738635, 0.13481396, 0.25115076, -0.009316611, -0.2584441, -0.14389823) * go_3(-1.0, 1.0);\n    result += mat4(-0.039723795, -0.14869407, -0.1692942, 0.026501274, -0.10685166, -0.121267825, -0.08584318, -0.09580693, -0.10626739, -0.068417974, 0.11321909, -0.13664317, 0.061380867, -0.2587898, 0.14850819, 0.008178645) * go_3(0.0, -1.0);\n    result += mat4(0.06912782, 0.24230564, -0.048150286, 0.2203717, -0.17417085, 0.105546735, -0.16648416, -0.0045053074, 0.09764028, 0.37122592, -0.1939995, -0.27899942, -0.088152565, -0.53869057, 0.21676709, -0.08056594) * go_3(0.0, 0.0);\n    result += mat4(0.07651754, 0.03704878, -0.0197015, 0.1660726, 0.07002748, -0.11820414, -0.23360898, 0.1481592, 0.029847002, 0.054057185, 0.013176299, 0.06552942, -0.13865773, -0.20105527, -0.37550658, 0.005769631) * go_3(0.0, 1.0);\n    result += mat4(-0.22697811, -0.17426412, 0.10148018, 0.008134666, 0.10771455, 0.16943407, -0.016319012, -0.40176705, -0.06854668, -0.049045276, 0.20919096, 0.13240765, -0.050125647, 0.14902508, 0.052697595, -0.13817468) * go_3(1.0, -1.0);\n    result += mat4(0.04301619, 0.23184754, -0.023551717, 0.3768405, 0.028999053, 0.06709736, -0.05993663, -0.059861984, 0.15499207, -0.22217415, 0.111131504, -0.09082529, -0.19389243, 0.024621522, -0.15305442, 0.010799284) * go_3(1.0, 0.0);\n    result += mat4(-0.035496738, 0.010802548, -0.028718363, 0.19263634, 0.16900502, -0.16661702, -0.027631328, 0.18309957, -0.015860107, -0.03309961, -0.091390446, 0.14000848, -0.0036591904, 0.47659522, -0.09373507, -0.29020965) * go_3(1.0, 1.0);\n    result += vec4(0.08895955, -0.027667087, 0.20500831, 0.00037762933);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!SAVE conv2d_5_tf\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.018134737, -0.2296755, -0.07276725, -0.029795367, 0.05382051, 0.092847414, -0.024469728, -0.1674685, 0.0017946451, 0.30074653, 0.0034195695, -0.04892261, 0.18229689, -0.20116119, -0.12702174, -0.08259108) * go_0(-1.0, -1.0);\n    result += mat4(-0.1357695, -0.08149211, 0.09314453, -0.21966846, 0.34740716, 0.043606415, 0.04225903, 0.034449834, 0.17248215, 0.39148283, -0.13868807, -0.010550686, 0.044238456, -0.09693464, -0.005044985, 0.24383289) * go_0(-1.0, 0.0);\n    result += mat4(0.19959371, 0.098685324, 0.058746945, 0.010580748, 0.08051514, 0.031898864, 0.017556064, 0.13004355, -0.01727653, 0.11044019, 0.040673427, -0.20064595, -0.23321067, 0.06398686, -0.19126236, -0.2430858) * go_0(-1.0, 1.0);\n    result += mat4(-0.12870286, -0.113455534, 0.23722827, 0.070718594, 0.19049989, -0.1927299, -0.06343845, 0.113127775, 0.082530305, -0.10972526, -0.090779535, 0.05731582, 0.11018802, -0.18049154, 0.09269507, -0.10304576) * go_0(0.0, -1.0);\n    result += mat4(0.15513484, 0.06659583, 0.08125296, -0.012350324, -0.09492788, 0.5048303, 0.13206847, 0.39554298, 0.28953737, -0.20913891, -0.26781562, -0.17539899, 0.023778774, 0.29716817, 0.15768486, 0.37702608) * go_0(0.0, 0.0);\n    result += mat4(0.0724462, 0.015571356, -0.032217246, 0.0050658924, -0.22708446, 0.03968809, 0.016753826, 0.0025668752, -0.055932112, 0.113931604, 0.19766758, -0.030027265, -0.17384295, 0.15013468, -0.0070017707, -0.09469028) * go_0(0.0, 1.0);\n    result += mat4(-0.078361556, -0.0954201, -0.006358101, 0.040500037, 0.4190454, -0.17622913, -0.07234791, 0.05462559, 0.18641087, 0.058313597, -0.0180785, 0.13818781, -0.14640772, 0.0699486, 0.0073663946, -0.076789856) * go_0(1.0, -1.0);\n    result += mat4(-0.21421191, 0.08736062, 0.09041226, 0.03608585, 0.02769972, 0.09641289, 0.11824623, 0.05653645, 0.16464607, 0.19839554, -0.13379547, 0.054417104, 0.067530684, 0.18971571, 0.13785432, -0.097639814) * go_0(1.0, 0.0);\n    result += mat4(-0.32658005, -0.14606023, -0.069448665, 0.032998275, -0.28331423, 0.0011900732, -0.020304207, -0.13535896, 0.08298347, 0.045509677, -0.030503955, -0.037504148, 0.049955815, 0.0925771, 0.00058534974, -0.12398032) * go_0(1.0, 1.0);\n    result += mat4(-0.2955836, 0.29059318, -0.018196672, -0.35866606, -0.01309431, 0.03540315, 0.010609202, 0.11956812, 0.10296229, 0.22536302, 0.015201129, -0.23797737, -0.16960852, -0.11414787, -0.034440614, 0.112644605) * go_1(-1.0, -1.0);\n    result += mat4(-0.14952518, 0.07024436, -0.083184876, -0.0814617, -0.13303639, 0.016159372, -0.13521518, 0.2221334, -0.056617837, 0.12958299, 0.064461656, -0.20146395, -0.16023181, 0.2640758, 0.27528805, -0.1426518) * go_1(-1.0, 0.0);\n    result += mat4(-0.04382363, 0.09856003, -0.08561442, -0.15699928, -0.121069774, 0.04685383, -0.009170197, -0.031489655, 0.18730178, 0.238442, 0.22497098, 0.032015145, -0.03709115, 0.1535079, 0.21674158, 0.10678019) * go_1(-1.0, 1.0);\n    result += mat4(-0.12200952, 0.24224263, 0.034097504, -0.028179523, -0.011962496, -0.04489487, -0.05198827, 0.22194928, -0.045400873, -0.049828544, 0.111477956, -0.098361604, 0.12788995, -0.016093334, -0.19886433, -0.011161484) * go_1(0.0, -1.0);\n    result += mat4(0.30563712, 0.013071727, -0.004799883, 0.12888052, -0.259498, -0.041566677, 0.07311124, 0.162324, 0.28371668, -0.004693743, -0.0019395344, 0.029358242, 0.08730285, 0.12184509, 0.05508437, 0.048439097) * go_1(0.0, 0.0);\n    result += mat4(0.12760857, 0.115813166, -0.217695, -0.10629871, -0.227366, 0.09030426, -0.15313712, 0.020528946, -0.20743734, 0.088583544, 0.04594053, -0.22891994, 0.18949282, -0.042186577, -0.17330512, -0.010711361) * go_1(0.0, 1.0);\n    result += mat4(0.029503195, 0.0063797613, -0.17004286, -0.096844055, 0.010218098, 0.04247233, 0.02362808, 0.14700809, -0.08082364, 0.11159672, -0.018505255, -0.15228583, 0.15693732, -0.025359154, 0.024829186, 0.1943192) * go_1(1.0, -1.0);\n    result += mat4(-0.03912932, -0.21989027, 0.12203028, 0.18702275, -0.118537985, 0.21039696, 0.09102061, 0.012288879, 0.031666897, 0.1318455, -0.04901404, -0.07516063, -0.44782668, 0.04884501, 0.047070876, 0.008728358) * go_1(1.0, 0.0);\n    result += mat4(-0.08669101, 0.3053463, -0.08963947, 0.0034188698, -0.070004664, 0.064788476, 0.093737036, 0.070050925, 0.12728429, -0.13179256, -0.014913502, 0.09308136, -0.027638942, 0.008638711, 0.08794172, -0.05531093) * go_1(1.0, 1.0);\n    result += mat4(0.0728421, 0.07872358, 0.11454748, 0.08497922, 0.071820416, -0.11789207, -0.08184197, 0.1359588, -0.2143346, -0.05876081, 0.023172129, -0.08430511, -0.19276723, 0.14283359, 0.15604696, -0.055187486) * go_2(-1.0, -1.0);\n    result += mat4(0.068641685, 0.2732106, -0.2809107, 0.12736696, -0.08642367, 0.023898933, -0.17859498, -0.18299665, -0.06684587, -0.12204666, 0.45898953, -0.24240111, 0.25182098, -0.04395751, 0.10637211, -0.22135144) * go_2(-1.0, 0.0);\n    result += mat4(0.0852072, 0.051133018, 0.03333165, -0.0008938216, 0.10251267, 0.0550774, 0.041769378, -0.21259712, 0.286912, 0.123342015, 0.282759, -0.0730124, 0.14275575, -0.15580742, -0.15224406, 0.045376908) * go_2(-1.0, 1.0);\n    result += mat4(0.03328225, 0.11563978, -0.07451964, 0.030546209, -0.04698351, -0.18544962, 0.037350416, 0.13969816, 0.0556746, -0.06359919, 0.06478219, -0.031694926, 0.13396506, 0.09443612, -0.01922686, -0.06290365) * go_2(0.0, -1.0);\n    result += mat4(0.07495407, 0.063429266, -0.106221214, -0.085107304, 0.2497817, -0.46598253, -0.18833177, -0.2731128, -0.13024822, 0.56053543, 0.055704467, -0.12331414, -0.031199086, 0.05061188, 0.22097112, -0.6611177) * go_2(0.0, 0.0);\n    result += mat4(0.08276988, -0.044184342, -0.03562185, -0.06159881, 0.27694225, -0.07192965, -0.08663714, 0.020221777, 0.14095962, -0.06229397, 0.051374253, -0.038158998, 0.10664802, -0.041305423, 0.051260717, -0.054698635) * go_2(0.0, 1.0);\n    result += mat4(0.12800686, 0.03485072, 0.039914366, 0.034041498, -0.08305794, -0.046292894, 0.22765331, 0.10904922, 0.0013937047, -0.08750301, 0.009126207, -0.065589435, 0.2837707, 0.08884436, -0.07234862, -0.093502745) * go_2(1.0, -1.0);\n    result += mat4(0.113439895, 0.06081726, 0.1122302, -0.022936966, 0.10329637, -0.31816107, -0.051597945, 0.23846027, -0.083913095, -0.29872265, -0.040147282, -0.08981918, -0.04329814, -0.12339693, -0.034489952, 0.013393211) * go_2(1.0, 0.0);\n    result += mat4(0.33091688, 0.1726297, 0.034332044, -0.091396205, 0.15434311, -0.0022870845, -0.15506189, 0.08710491, -0.16063525, 0.042252056, 0.017086457, 0.08134797, 0.08631321, 0.037843138, 0.088296555, 0.0064518084) * go_2(1.0, 1.0);\n    result += mat4(0.09161051, 0.114355795, -0.15304486, -0.030537153, 0.1835368, -0.3287635, 0.031197926, 0.09717476, 0.04276852, 0.113250345, 0.05949038, -0.10599563, 0.43574792, -0.060788117, 0.18409383, 0.12678055) * go_3(-1.0, -1.0);\n    result += mat4(-0.018356865, -0.0072578182, 0.12020777, -0.013127592, 0.20136636, -0.22984362, 0.06896224, 0.00044982752, 0.008428429, -0.123316936, -0.09989286, 0.078248784, -0.16313677, -0.003020313, -0.46285018, -0.08967125) * go_3(-1.0, 0.0);\n    result += mat4(-0.03451497, -0.10864502, 0.13207638, 0.17194521, 0.0037514758, -0.20222199, -0.12535086, 0.001511977, 0.056294486, -0.2112898, 0.078261316, 0.10118746, -0.044742294, 0.21793383, -0.19927903, -0.21338293) * go_3(-1.0, 1.0);\n    result += mat4(-0.034903776, -0.10167085, 0.031066334, 0.0379958, 0.20532596, -0.17457838, 0.16556816, -0.0021619152, 0.02682665, 0.03396325, -0.059273884, 0.1922813, -0.072151475, -0.010240544, 0.2302027, 0.12385962) * go_3(0.0, -1.0);\n    result += mat4(-0.20170145, -0.08203941, -0.028107846, -0.18003726, 0.44744352, -0.13190243, 0.13233365, 0.03626546, 0.085763134, -0.25613126, -0.11213388, 0.15529087, -0.271649, 0.050587676, -0.062583975, 0.057289865) * go_3(0.0, 0.0);\n    result += mat4(-0.040649455, -0.17949733, 0.35847965, -0.040587306, 0.24314344, -0.23811667, 0.13958354, 0.04961874, 0.09858903, -0.04202913, -0.21850993, 0.0700419, -0.09130745, -0.096835814, 0.0022782686, -0.25416258) * go_3(0.0, 1.0);\n    result += mat4(-0.08215545, -0.019647893, 0.055263475, 0.053733055, 0.098485716, -0.1041945, -0.06541415, -0.08868577, -0.07262986, 0.03513784, -0.110529095, -0.03369232, 0.056786604, 0.2569229, -0.05931065, -0.22081214) * go_3(1.0, -1.0);\n    result += mat4(0.066926084, 0.029664058, -0.10779271, 0.11026963, 0.23927264, -0.16914488, 0.022947345, 0.12303853, -0.07066212, -0.013205378, 0.15348643, 0.035568032, 0.20966691, 0.010149819, -0.08814468, -0.064854674) * go_3(1.0, 0.0);\n    result += mat4(0.11493852, -0.074924305, -0.14840698, -0.16956823, 0.056806292, -0.06387947, -0.06880271, -0.04637334, -0.1929893, 0.18226422, 0.064644486, -0.1594863, 0.027403917, 0.13951495, -0.06569123, -0.07700207) * go_3(1.0, 1.0);\n    result += vec4(-0.043347504, -0.20504741, -0.037821215, -0.014486937);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!SAVE conv2d_5_tf1\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.047881734, -0.09396414, -0.2839081, 0.3140853, 0.052613556, 0.09940423, 0.23960467, -0.022228222, -0.12065009, 0.07898222, 0.08657881, 0.010852739, -0.050450284, 0.01683982, 0.031813968, 0.053060856) * go_0(-1.0, -1.0);\n    result += mat4(-0.10252411, -0.03116448, -0.30114275, -0.0316799, -0.017501019, -0.03006003, -0.2095696, 0.10134927, -0.3901916, -0.15335023, -0.11955071, 0.1337449, 0.101239376, -0.25044814, 0.2128469, 0.018979514) * go_0(-1.0, 0.0);\n    result += mat4(-0.13392173, 0.052036732, 0.1682114, -0.026263753, 0.027221246, -0.15121374, 0.13723798, 0.08950682, -0.1182108, -0.07294226, 0.023392374, 0.052329235, -0.05632852, -0.07036173, 0.06872573, 0.05238042) * go_0(-1.0, 1.0);\n    result += mat4(0.18112028, 0.18242362, -0.06812871, 0.032463413, 0.124638766, -0.26765212, -0.07678663, 0.33806562, 0.09674393, 0.15574542, 0.23634006, -0.02873782, -0.1626769, -0.14760062, -0.007274849, 0.09866139) * go_0(0.0, -1.0);\n    result += mat4(-0.10726673, -0.10925056, 0.19967109, -0.19936769, 0.15942842, -0.14870064, 0.15493345, -0.08489036, -0.49053356, -0.17321263, 0.28426084, 0.18721215, -0.09898434, -0.2751838, -0.11833524, 0.028445128) * go_0(0.0, 0.0);\n    result += mat4(-0.11788817, -0.23724948, -0.046072144, 0.035621114, 0.04527003, -0.0073492974, 0.11097195, 0.06806836, 0.04814677, -0.1408476, -0.1325629, 0.00929532, -0.16699041, -0.03034791, 0.08320368, -0.15429299) * go_0(0.0, 1.0);\n    result += mat4(0.2729515, 0.008244692, -0.17441982, -0.39026466, 0.17381759, 0.31194404, 0.055934936, 0.20744409, 0.20119062, 0.0734271, 0.0796807, 0.0031037466, -0.0016392237, 0.033733975, 0.07149338, 0.042083208) * go_0(1.0, -1.0);\n    result += mat4(0.07985744, 0.10945015, 0.018472541, 0.1397503, 0.2005682, 0.42641, 0.23022486, -0.2916921, 0.028285174, -0.31885162, -0.27070364, -0.10390779, 0.0751492, 0.12752363, -0.2279459, 0.08998453) * go_0(1.0, 0.0);\n    result += mat4(0.18450491, -0.140783, -0.008006845, 0.09029298, 0.12536179, 0.26949662, 0.09491545, 0.063907005, 0.11212244, 0.09778506, -0.1835966, -0.053119674, 0.0072294096, 0.25018227, 0.010868525, -0.22721334) * go_0(1.0, 1.0);\n    result += mat4(-0.028011927, -0.20073172, 0.5976166, -0.19494139, 0.17958745, -0.03838646, 0.058325976, -0.29409218, -0.12793432, 0.03245129, 0.35662368, -0.05048354, -0.13368197, -0.06151968, -0.012714591, -0.1763054) * go_1(-1.0, -1.0);\n    result += mat4(0.18468465, 0.31682113, 0.12818255, -0.117110476, 0.13709468, -0.10034022, -0.07994527, -0.1259309, 0.04067299, -0.1147398, 0.28361055, 0.27916273, 0.03696692, 0.16829546, 0.27819383, 0.08305029) * go_1(-1.0, 0.0);\n    result += mat4(-0.28920117, -0.033877946, 0.01586206, 0.04681198, 0.024248574, -0.045777842, -0.03342128, 0.07525412, -0.063377544, -0.016737273, 0.11235511, -0.04325238, -0.24170023, -0.09993599, -0.03205371, 0.14339828) * go_1(-1.0, 1.0);\n    result += mat4(-0.008357902, -0.11038377, 0.03709221, 0.26775306, 0.07963845, -0.25377446, -0.17630441, -0.10966474, 0.057311732, -0.083327, 0.044497233, 0.06903858, -0.26531395, -0.103399664, -0.14806591, 0.269314) * go_1(0.0, -1.0);\n    result += mat4(0.05450808, -0.041993964, -0.07217651, 0.034468375, 0.2117634, 0.0075620585, 0.05825411, -0.2252478, -0.0527787, 0.049732126, -0.032040413, -0.09361454, 0.29585132, 0.018413153, 0.18384546, -0.024226356) * go_1(0.0, 0.0);\n    result += mat4(-0.031109914, 0.19351351, 0.07405522, -0.06313074, -0.09983541, -0.011495182, 0.11749038, -0.16775608, 0.2790974, -0.09338754, 0.07913264, 0.103792936, -0.18679164, -0.15639925, 0.112943865, 0.07930375) * go_1(0.0, 1.0);\n    result += mat4(0.004106195, -0.036833283, 0.12908752, 0.12869535, -0.02472107, 0.17561707, -0.025890926, -0.18789047, 0.096218705, -0.16306408, -0.02198454, -0.010134957, -0.09710009, 0.002062143, -0.046785697, 0.0029441968) * go_1(1.0, -1.0);\n    result += mat4(0.19648251, -0.015663045, -0.0730215, 0.028611008, 0.13529862, -0.015256192, -0.04119306, -0.24628192, 0.02601027, -0.21184283, -0.1962902, 0.09109358, -0.06792383, 0.092336476, 0.12215351, -0.08596062) * go_1(1.0, 0.0);\n    result += mat4(-0.17530201, -0.0351919, -0.31872514, -0.13933206, -0.07000922, -0.049807087, 0.0010997375, -0.033573963, 0.07442056, -0.33290103, -0.40381998, 0.09435, -0.3280128, -0.09953127, -0.11283648, 0.20685865) * go_1(1.0, 1.0);\n    result += mat4(-0.052573867, -0.035328753, -0.11132943, -0.17515652, 0.05021051, 0.058642425, -0.046640664, 0.0799107, -0.027398815, -0.33619994, -0.22135767, 0.07894002, -0.14941697, -0.0940996, -0.11655085, 0.049795926) * go_2(-1.0, -1.0);\n    result += mat4(-0.039301276, 0.041062318, 0.20312686, -0.009338705, 0.013706282, -0.0245852, 0.03458311, 0.09601228, -0.18203016, -0.012260314, 0.17984508, -0.056576703, -0.102844186, 0.24047872, 0.05307189, 0.16066082) * go_2(-1.0, 0.0);\n    result += mat4(0.1478775, 0.0046362123, 0.05459521, 0.07162838, -0.01896149, 0.23700175, -0.14174299, 0.06988599, -0.32545477, -0.08065096, -0.061227743, -0.0010796773, 0.094327345, -0.20760082, -0.19523263, 0.19859222) * go_2(-1.0, 1.0);\n    result += mat4(-0.049676366, -0.10381536, 0.02546116, -0.13127093, 0.10954914, 0.0048147943, 0.06962328, -0.30456528, -0.11956627, 0.0150488885, -0.10711722, 0.1684613, -0.1939089, -0.10577047, -0.11980919, -0.036988296) * go_2(0.0, -1.0);\n    result += mat4(-0.054795764, 0.09491116, -0.08494948, 0.059765853, 0.0131597435, 0.20786162, 0.11999637, 0.024381055, 0.22830428, 0.027053319, -0.011646274, -0.12145409, -0.07899559, -0.012688263, 0.10684157, 0.3824219) * go_2(0.0, 0.0);\n    result += mat4(-0.23994572, -0.0031532666, -0.0050638164, 0.14236279, 0.05690383, -0.06259682, 0.052624144, 0.20461404, -0.19230312, -0.11072268, 0.013023965, 0.08931543, -0.21997221, 0.11760443, -0.40943825, 0.28656834) * go_2(0.0, 1.0);\n    result += mat4(-0.06606179, 0.26007771, 0.033754125, 0.119690455, 0.024669139, -0.06752839, 0.12688096, -0.0063201943, -0.17123021, 0.07548857, -0.14213699, 0.034093797, -0.15632647, -0.123243414, -0.42634043, 0.1715022) * go_2(1.0, -1.0);\n    result += mat4(-0.046503466, 0.13876389, 0.17973013, -0.25938338, -0.18824704, -0.11876702, 0.31065792, -0.041042212, -0.061369427, 0.2057992, 0.17295738, 0.3836555, -0.21109799, -0.10167118, 0.16577047, 0.113483034) * go_2(1.0, 0.0);\n    result += mat4(-0.24534856, -0.014482421, 0.22515748, -0.12773542, 0.12794174, -0.02528619, 0.41710484, 0.09154934, -0.17805946, -0.25428918, 0.07294183, 0.047079418, -0.30949152, -0.08919157, 0.17888431, 0.17706038) * go_2(1.0, 1.0);\n    result += mat4(-0.1741826, 0.046225294, -0.10761791, 0.2619953, 0.007373745, 0.05104337, -0.22309966, 0.34529984, -0.034363825, -0.022187237, -0.08609555, 0.16842419, 0.28136057, 0.17843607, -0.11307746, -0.05668021) * go_3(-1.0, -1.0);\n    result += mat4(-0.12310616, -0.29661375, -0.10581025, -0.049584012, 0.19651765, 0.08436489, -0.14533581, -0.029874112, -0.15422897, -0.062741704, -0.22694711, -0.15547274, -0.15181333, 0.0286061, 0.022438493, -0.062447168) * go_3(-1.0, 0.0);\n    result += mat4(0.3497046, -0.09455009, 0.060618952, -0.2134236, 0.054515295, 0.07451165, -0.09267233, -0.010513333, 0.13842636, 0.11563433, -0.054750167, 0.050432, 0.1514256, 0.04284002, -0.2095581, 0.07907657) * go_3(-1.0, 1.0);\n    result += mat4(-0.11745651, -0.04717057, 0.085377194, -0.065956995, 0.07280491, 0.2730059, 0.11088276, 0.2437957, 0.14018989, 0.1164107, -0.09516929, 0.0022427947, 0.111544006, -0.0680495, 0.09324579, -0.12482022) * go_3(0.0, -1.0);\n    result += mat4(-0.07995795, -0.03387884, 0.019846136, 0.10231208, -0.07017192, 0.18659039, 0.035161644, 0.101182766, -0.14901665, 0.21307294, 0.063894205, -0.27546507, -0.24792959, -0.067731075, 0.13146006, -0.19333683) * go_3(0.0, 0.0);\n    result += mat4(0.034206454, 0.1472648, -0.07406727, 0.014654025, 0.18703444, 0.1319857, -0.10610886, 0.08427947, -0.017536618, -0.06487879, -0.12095286, -0.050414838, 0.03260879, 0.1558894, -0.031887084, 0.11840288) * go_3(0.0, 1.0);\n    result += mat4(0.114811294, -0.14574333, -0.09392587, 0.042283528, 0.08919092, 0.18259068, 0.0980717, 0.21024778, -0.1280008, -0.027260462, -0.1129027, 0.18722472, 0.13733985, 0.047153983, 0.030871978, 0.1998385) * go_3(1.0, -1.0);\n    result += mat4(-0.06783575, 0.004612595, 0.1153467, -0.11531557, -0.048889533, 0.07673577, -0.02041786, 0.22744459, -0.13092506, 0.13484807, 0.40003043, -0.053706612, -0.16985156, -0.04791236, -0.052443005, -0.08363625) * go_3(1.0, 0.0);\n    result += mat4(0.18187882, 0.017893985, 0.17856054, 0.005413129, 0.014147176, 0.15102178, 0.12436294, -0.02176765, -0.16727823, -0.0364111, 0.17074408, 0.12899421, 0.31984514, -0.0072070034, 0.031895883, -0.1991405) * go_3(1.0, 1.0);\n    result += vec4(-0.011865144, 0.11717201, -0.13823777, -0.059450272);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!SAVE conv2d_6_tf\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.082203194, 0.021720003, 0.03725474, -0.08048348, 0.2063248, -0.033020593, -0.17585336, 0.06476272, 0.012244563, 0.026554609, 0.014708393, 0.26606125, 0.14248778, 0.12817341, -0.039826933, -0.12751861) * go_0(-1.0, -1.0);\n    result += mat4(0.24573852, 0.19695967, -0.06257417, -0.04782871, 0.3511875, -0.018083302, -0.077342674, 0.15247667, 0.20321761, -0.07479984, -0.09548503, 0.08109568, -0.23808748, 0.07246303, -0.004242619, 0.16162953) * go_0(-1.0, 0.0);\n    result += mat4(0.13296306, 0.19495387, 0.009222276, 0.033592198, 0.20443891, 0.16063854, -0.2581601, -0.016132578, -0.2296461, -0.23647323, -0.15407176, -0.18265317, 0.2343241, -0.049697313, -0.09398783, 0.41931856) * go_0(-1.0, 1.0);\n    result += mat4(-0.10866088, -0.40605694, -0.0042648134, 0.07943803, 0.26914695, 0.14816476, 0.037706107, -0.123223364, -0.19962949, -0.053534556, -0.08397409, -0.04244924, -0.075791344, 0.29629225, 0.2311928, 0.099177904) * go_0(0.0, -1.0);\n    result += mat4(-0.1748319, -0.2003186, -0.32659066, -0.21007413, 0.20122464, 0.032196607, -0.026299698, 0.33395135, 0.11411664, 0.05971959, 0.09001304, -0.15936212, 0.012322024, 0.19936106, -0.411186, -0.08319479) * go_0(0.0, 0.0);\n    result += mat4(-0.07349218, 0.006184436, 0.096199185, -0.050186496, 0.064047046, -0.03813128, -0.057007037, -0.025550695, -0.2863145, -0.008512981, -0.20615962, 0.18009211, 0.008298396, 0.22452813, 0.010843521, 0.20169461) * go_0(0.0, 1.0);\n    result += mat4(0.2691149, 0.059546687, 0.08922005, 0.2252196, 0.30341956, -0.024489028, 0.087045394, -0.03856442, -0.14083561, -0.17683443, 0.14137806, 0.15520614, 0.2073925, -0.19525874, 0.23661858, 0.3098405) * go_0(1.0, -1.0);\n    result += mat4(0.006530723, 0.04180736, -0.04762067, -0.064395495, 0.02396811, -0.13332283, 0.0037775645, 0.026309434, 0.0033065109, -0.08315753, 0.02917419, 0.12330464, 0.22819455, -0.07489677, 0.12829056, -0.097994626) * go_0(1.0, 0.0);\n    result += mat4(-0.09983759, 0.032783493, 0.11085758, 0.08993078, -0.057110567, -0.018973934, -0.14946178, -0.03921629, 0.039757587, 0.015860094, 0.04989561, -0.19634786, 0.04351146, 0.019315343, 0.25972188, 0.17989321) * go_0(1.0, 1.0);\n    result += mat4(-0.04111906, -0.165601, 0.0003682197, -0.056232415, -0.32716644, -0.24015541, -0.057547837, 0.05966729, 0.06854747, 0.03599213, -0.18798864, 0.1183447, 0.014268468, -0.1310834, 0.06415977, -0.19414157) * go_1(-1.0, -1.0);\n    result += mat4(-0.00070661673, 0.17671427, 0.10584568, -0.060910843, -0.104282066, -0.22676118, -0.01907062, 0.24882245, -0.043454725, 0.07691623, -0.48371696, 0.013537671, -0.025488405, 0.061228953, 0.18548754, 0.028671112) * go_1(-1.0, 0.0);\n    result += mat4(-0.0121596735, 0.09595702, -0.08244918, -0.1176173, 0.26773354, -0.021729136, 0.075465776, -0.0928876, 0.12461298, 0.16830076, -0.15302569, 0.113850676, 0.09811088, 0.13006307, 0.24999009, 0.10261325) * go_1(-1.0, 1.0);\n    result += mat4(-0.032246377, 0.038265374, -0.26476422, -0.1442876, -0.19866082, 0.08649541, 0.041478764, 0.11155026, 0.21576422, -0.09572912, -0.11174068, -0.19722937, -0.15801935, 0.29604745, -0.08606268, -0.15532136) * go_1(0.0, -1.0);\n    result += mat4(-0.06315591, 0.16151646, -0.009230362, -0.04341246, 0.09085519, 0.21924476, 0.38044852, 0.193819, 0.16622902, 0.0025134624, -0.22688466, -0.025276015, 0.07714917, 0.16302192, -0.11767101, -0.11086476) * go_1(0.0, 0.0);\n    result += mat4(-0.04170153, 0.001859292, -0.26352355, 0.10982333, -0.031867817, 0.15773517, -0.060263418, 0.11117763, -0.017359972, 0.0127261225, 0.0782802, -0.16908924, 0.080516845, -0.05691526, -0.07530135, -0.14553802) * go_1(0.0, 1.0);\n    result += mat4(0.06112685, -0.032287434, 0.17445667, -0.044935808, -0.11449107, -0.051394563, -0.029589338, -0.14555557, 0.03440661, 0.11035615, -0.17175, -0.14851089, 0.037362, -0.18740481, 0.17278154, 0.18073405) * go_1(1.0, -1.0);\n    result += mat4(-0.27670652, 0.19484822, 0.2609349, 0.1455016, 0.04438468, 0.1449185, 0.11185832, -0.18598269, -0.019846648, 0.11886126, -0.098498635, 0.15737785, 0.011406795, -0.18860829, -0.13705735, 0.17535745) * go_1(1.0, 0.0);\n    result += mat4(-0.30244905, -0.28695273, 0.1146976, 0.21144345, -0.037980128, -0.027679864, -0.13992494, -0.04884521, -0.032023884, -0.07921183, -0.16042095, -0.06935386, -0.06570237, -0.1107404, -0.018163798, 0.22625941) * go_1(1.0, 1.0);\n    result += mat4(-0.07292955, -0.07321777, -0.045146503, -0.33291966, -0.096732594, -0.07203495, 0.33692798, 0.2870733, 0.122160144, -0.076574564, 0.042844944, 0.26448342, 0.07672146, -0.028775277, -0.12088313, 0.15583947) * go_2(-1.0, -1.0);\n    result += mat4(0.21589327, 0.05258274, 0.09705794, -0.024653846, -0.039402515, 0.28485695, 0.14711736, -0.10556087, -0.15140481, 0.09039498, 0.017308712, 0.11862922, 0.08230978, 0.21678248, -0.043815188, -0.226433) * go_2(-1.0, 0.0);\n    result += mat4(-0.029258793, 0.26618922, 0.02564014, -0.23189862, -0.24074338, -0.18556763, 0.25973624, 0.04746873, 0.0137007125, -0.22239363, -0.12414957, 0.048228756, -0.22406264, 0.282667, -0.021001073, -0.17465611) * go_2(-1.0, 1.0);\n    result += mat4(0.32401654, -0.1495363, -0.20869227, 0.04271639, -0.0087802755, 0.031325378, 0.23834595, 0.039336167, 0.17265107, 0.20947595, 0.28737286, 0.0028783784, -0.057340365, -0.050347418, -0.11915604, -0.1831807) * go_2(0.0, -1.0);\n    result += mat4(0.1811338, 0.07732653, 0.20975596, -0.47129005, 0.07121942, 0.08410583, 0.44170937, -0.19524159, -0.17807977, 0.12837476, 0.20816846, -0.1741958, -0.04411918, 0.06024972, 0.18159702, -0.052485272) * go_2(0.0, 0.0);\n    result += mat4(-0.15229738, 0.27513, 0.28150418, -0.19543962, -0.02045864, -0.07207227, 0.09589587, 0.09110817, 0.061413247, 0.0046052113, 0.11619411, -0.2988938, 0.065739445, 0.10205611, 0.12847126, -0.028355654) * go_2(0.0, 1.0);\n    result += mat4(0.0657154, -0.047568597, -0.16148911, 0.16392621, -0.25281775, -0.061153214, 0.017480455, -0.026288848, 0.20319715, 0.04763355, 0.010444491, -0.26671803, -0.25821987, 0.32863674, -0.30734694, -0.18190521) * go_2(1.0, -1.0);\n    result += mat4(-0.042703815, 0.06633036, -0.048434302, -0.17176376, -0.12699759, -0.1124558, 0.083266065, 0.03354623, -0.13468939, 0.12706263, 0.053659134, -0.06930602, 0.008196115, 0.2034998, -0.06351442, -0.039730288) * go_2(1.0, 0.0);\n    result += mat4(0.09614661, 0.22500272, 0.088511504, -0.16960482, 0.15364788, -0.18854137, -0.13163191, -0.07503735, -0.23177068, -0.0053305267, -0.041978605, 0.0971947, -0.049034655, 0.04486706, 0.09076307, -0.02310868) * go_2(1.0, 1.0);\n    result += mat4(-0.1304683, 0.17743458, -0.09817326, -0.0646786, 0.07886976, 0.20109388, -0.034114968, -0.2029261, -0.03348398, 0.029337432, -0.07302782, -0.02240758, 0.030242773, -0.30032325, 0.02085572, -0.027314361) * go_3(-1.0, -1.0);\n    result += mat4(-0.037377544, 0.026350772, -0.07430488, -0.114671774, -0.126935, -0.046512567, -0.033628833, -0.19018382, -0.041053895, -0.031206857, 0.08562848, -0.01875709, 0.21099389, -0.092511, 0.0073047103, -0.009811013) * go_3(-1.0, 0.0);\n    result += mat4(0.11358029, 0.17468451, -0.12739041, -0.14332245, -0.22230148, 0.16862972, -0.04462456, 0.2469604, -0.008622369, 0.0081848325, -0.17032363, -0.16024362, 0.21178265, 0.037127133, 0.08559072, 0.11584694) * go_3(-1.0, 1.0);\n    result += mat4(0.008993893, -0.08037705, 0.4426555, 0.15593371, 0.15273719, -0.03249998, 0.055109, -0.1512612, -0.037183985, 0.20825677, -0.08516227, -0.06664223, -0.10011001, -0.3505215, -0.17941694, 0.052089088) * go_3(0.0, -1.0);\n    result += mat4(-0.109703645, -0.13505603, 0.1336451, 0.13118869, 0.010915504, 0.12748592, 0.21201555, -0.40841985, -0.11059143, 0.033772044, -0.039282143, 0.03095394, 0.10394723, -0.21343367, -0.10699851, -0.028351074) * go_3(0.0, 0.0);\n    result += mat4(0.019704714, 0.06243651, 0.09896519, -0.17492259, 0.012675787, -0.004239029, 0.21319824, 0.069183126, -0.0071114586, 0.123431124, -0.24479835, 0.00723795, -0.045293927, 0.014101029, 0.15746681, 0.042405806) * go_3(0.0, 1.0);\n    result += mat4(0.023828225, -0.0015190929, 0.1194638, 0.082163885, 0.10532113, 0.042044062, 0.02528007, 0.015175004, 0.026613194, 0.33525538, -0.1627064, -0.29887968, -0.197707, 0.038967777, -0.15811683, -0.106895216) * go_3(1.0, -1.0);\n    result += mat4(0.044362027, -0.04946742, -0.14815849, -0.17660522, -0.034201477, -0.012243106, -0.050183997, 0.06407372, 0.039822515, 0.15880872, -0.0672721, -0.4081093, 0.019489579, -0.060278706, -0.015096743, -0.07799167) * go_3(1.0, 0.0);\n    result += mat4(0.11861756, 0.27113584, -0.14107186, -0.10246008, -0.124051, -0.1627854, 0.10698585, 0.2846401, -0.061731786, 0.1724438, -0.12428688, -0.09986041, -0.034171514, -0.07100923, 0.041739646, -0.11308375) * go_3(1.0, 1.0);\n    result += vec4(-0.02981662, -0.26338395, -0.011632586, 0.15063232);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!SAVE conv2d_6_tf1\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.17082009, 0.031344634, -0.06131912, 0.00887183, -0.01528174, 0.12943709, 0.24537678, 0.008178781, -0.312396, -0.023583878, 0.07827866, -0.1231261, 0.15081584, -0.18161978, -0.25179705, -0.036934935) * go_0(-1.0, -1.0);\n    result += mat4(-0.05768411, 0.16785417, -0.1788644, -0.0067257965, 0.021445744, 0.10066516, -0.23864186, 0.1450302, 0.12892793, 0.19856106, -0.24444748, 0.16531628, -0.044425935, -0.02775357, 0.009059946, -0.12958384) * go_0(-1.0, 0.0);\n    result += mat4(-0.025798557, -0.17238182, -0.34056288, -0.20921059, -0.03576266, 0.1476854, -0.06264234, 0.14452787, -0.04130045, -0.07275762, 0.034578666, 0.2914669, 0.20879944, 0.21359251, -0.048695553, 0.2638088) * go_0(-1.0, 1.0);\n    result += mat4(-0.022791177, 0.4204545, 0.116855636, 0.20241925, -0.010444933, -0.14462502, 0.022550104, -0.24423064, -0.09417524, 0.045358784, -0.11405829, 0.035979558, -0.2283092, -0.06670842, -0.23852053, -0.22417003) * go_0(0.0, -1.0);\n    result += mat4(-0.14526704, 0.040880535, 0.14076385, 0.07795045, -0.059177604, -0.13056375, -0.3373641, -0.19344307, -0.29891858, -0.32578763, -0.29061425, 0.1562214, -0.13578376, 0.36586633, 0.24936736, 0.054629393) * go_0(0.0, 0.0);\n    result += mat4(-0.025790233, -0.13020341, -0.10084969, 0.15767297, -0.09738769, 0.04034404, 0.0038675873, 0.043515608, 0.16899958, -0.29117966, 0.03420067, 0.14432564, -0.10473084, 0.21014084, 0.07775908, -0.09303797) * go_0(0.0, 1.0);\n    result += mat4(-0.07443987, -0.16225167, 0.036251917, 0.028432872, 0.03759333, 0.004027401, -0.033941846, 0.0019474924, 0.02357054, 0.30748722, 0.1652115, -0.17361522, 0.16905582, 0.08048018, -0.23639561, -0.029408466) * go_0(1.0, -1.0);\n    result += mat4(0.0461233, -0.09346199, -0.07063276, -0.19447634, -0.049339604, -0.0032855074, -0.22661209, -0.0543389, 0.11924857, -0.21691081, -0.1645725, -0.0075736847, 0.018572787, -0.06552861, -0.01777661, -0.11651732) * go_0(1.0, 0.0);\n    result += mat4(-0.06425901, 0.123392984, -0.16395192, -0.093448035, -0.029316641, 0.0986573, -0.23135012, 0.011170849, 0.00023920486, 0.15296175, 0.35453254, -0.05189021, 0.20708887, -0.103900835, 0.081992395, -0.21829562) * go_0(1.0, 1.0);\n    result += mat4(-0.019074136, -0.1572586, 0.27919227, 0.09119617, 0.035954695, 0.2941489, 0.18262725, -0.055522963, -0.21364328, -0.1573611, 0.104966134, 0.08228523, 0.19945285, -0.0039229114, -0.1565048, 0.028975379) * go_1(-1.0, -1.0);\n    result += mat4(-0.18501253, 0.006473006, 0.06637501, 0.04295065, 0.06411007, 0.1166344, -0.10060226, 0.46296063, -0.08600344, -0.03560105, 0.012215349, 0.017885283, 0.061346993, 0.17336361, 0.01935021, 0.20198092) * go_1(-1.0, 0.0);\n    result += mat4(-0.04451627, -0.10372061, -0.13968691, 0.14479733, 0.1660607, 0.19334625, 0.0085214665, 0.28863636, -0.07600901, -0.014777084, 0.13209191, -0.09045013, 0.104893915, -0.04776884, -0.007936376, 0.104568765) * go_1(-1.0, 1.0);\n    result += mat4(0.023751335, -0.108048, -0.050531313, 0.15916029, 0.13246661, 0.04644228, -0.09586482, -0.17222965, -0.22898191, -0.033484615, 0.078883134, -0.052609313, -0.2721741, 0.045986425, 0.13972299, -0.28923607) * go_1(0.0, -1.0);\n    result += mat4(-0.23364568, -0.008875902, -0.40894926, 0.060443908, -0.2839635, -0.5270991, -0.2500865, 0.002020195, -0.24488612, -0.04982319, -0.009110353, -0.018023955, 0.06647274, -0.25225738, 0.26154432, -0.033934146) * go_1(0.0, 0.0);\n    result += mat4(-0.1535129, -0.21257545, -0.16553773, 0.17471452, -0.06203719, 0.15238857, 0.18702018, 0.18572305, 0.07740396, -0.074217625, -0.072156586, -0.2183728, 0.00403749, 0.13750519, 0.30362993, 0.06550286) * go_1(0.0, 1.0);\n    result += mat4(0.37164542, -0.1980723, -0.15659203, 0.19498909, 0.01748114, 0.011807152, -0.05424202, 0.11926474, 0.050406165, -0.12925303, -0.020280985, 0.08429331, 0.14769496, -0.077555746, -0.15216178, -0.27070466) * go_1(1.0, -1.0);\n    result += mat4(0.35804263, 0.08539285, -0.14785156, -0.13532467, 0.058254432, 0.20448379, -0.006173341, 0.058168225, -0.21714899, -0.13472849, -0.09392532, -0.12753737, -0.097461835, -0.11419082, 0.09384189, 0.06414768) * go_1(1.0, 0.0);\n    result += mat4(0.023494452, -0.22187226, -0.16694295, 0.0204334, -0.26720086, 0.15916729, 0.3098874, -0.10292057, 0.008854983, 0.13375004, -0.04409455, 0.09286524, 0.095829524, 0.12427317, -0.048659876, 0.18300754) * go_1(1.0, 1.0);\n    result += mat4(-0.119153984, 0.10163183, 0.025017537, -0.40096784, 0.026778705, 0.15821172, -0.19947284, -0.33337715, 0.2952563, 0.16820388, -0.057061996, -0.029319009, -0.12184868, 0.09031512, 0.12028806, 0.021044692) * go_2(-1.0, -1.0);\n    result += mat4(0.086744264, -0.046958666, 0.2130253, -0.46672252, 0.07135636, 0.0100029735, -0.13828261, -0.012365689, -0.11374441, 0.21084632, -0.059631422, -0.013799735, -0.037889663, -0.10701892, -0.09493782, 0.15516634) * go_2(-1.0, 0.0);\n    result += mat4(0.031181194, -0.01535001, 0.029270316, 0.13128386, 0.11838377, -0.17051528, 0.12228499, -0.04841128, 0.33350074, -0.006144013, -0.09055018, 0.27470216, -0.26665646, -0.08703671, -0.01719071, -0.23449609) * go_2(-1.0, 1.0);\n    result += mat4(-0.12856458, 0.005562174, -0.19517267, 0.13270985, 0.2776414, 0.032003902, -0.15778573, 0.15344355, 0.26930434, -0.13459459, 0.035019353, 0.08896612, 0.12847935, -0.122637205, 0.001815178, 0.08290523) * go_2(0.0, -1.0);\n    result += mat4(0.33805037, -0.15318587, -0.20955376, -0.26121393, -0.026022578, -0.1617741, 0.1336867, 0.026223289, 0.012059392, -0.17295446, -0.060811974, 0.14027825, -0.21134059, -0.08408573, -0.23773228, 0.110836074) * go_2(0.0, 0.0);\n    result += mat4(0.16176093, 0.15307428, -0.07711325, -0.3458805, 0.061291527, 0.023916256, 0.21370678, 0.0015756418, 0.10642374, 0.24807373, 0.11164451, 0.10780487, 0.087194376, -0.2718231, -0.008457387, 0.054078236) * go_2(0.0, 1.0);\n    result += mat4(-0.03259038, -0.20923306, 0.165477, 0.098864526, -0.02734457, 0.08871225, -0.01552188, 0.047712058, 0.055032052, -0.13044262, -0.2899521, 0.22230095, -0.029343741, -0.16427459, -0.005436118, -0.05111821) * go_2(1.0, -1.0);\n    result += mat4(0.20065974, -0.1556366, -0.12620135, 0.44572976, -0.020925352, 0.12025185, 0.20588058, 0.06391864, 0.046870507, 0.16942503, -0.049370963, 0.008779016, 0.04954915, 0.090298936, -0.16466027, 0.011152038) * go_2(1.0, 0.0);\n    result += mat4(0.13587528, 0.047841422, 0.19804007, -0.1672396, -0.072491, 0.04543739, 0.25287256, 0.015226213, 0.02007356, -0.049578942, -0.08796175, 0.1714897, -0.07819061, 0.1509537, 0.093094915, 0.031139288) * go_2(1.0, 1.0);\n    result += mat4(-0.013774682, 0.118201815, -0.009592314, -0.10837201, -0.0686881, -0.083380274, 0.107689425, 0.046642892, 0.119898744, -0.05502989, -0.19719897, 0.0005697584, -0.0921928, 0.032281205, 0.2568853, 0.2325449) * go_3(-1.0, -1.0);\n    result += mat4(0.02991112, -0.09898633, 0.06076172, -0.20906185, 0.0026118348, 0.06130956, 0.06760944, -0.16662054, 0.065741204, -0.13144116, 0.011419801, 0.22552124, 0.1465757, -0.07417319, -0.10788749, -0.24952699) * go_3(-1.0, 0.0);\n    result += mat4(-0.19238451, -0.024058497, 0.19580396, -0.067399554, -0.18832864, -0.11752747, -0.078949094, -0.23762032, -0.04141864, 0.022530237, -0.02222157, 0.0054874527, 0.057746816, -0.34854797, 0.028730657, -0.08976777) * go_3(-1.0, 1.0);\n    result += mat4(0.16888975, 0.19949849, -0.08456147, -0.03619044, -0.019596824, 0.11214634, 0.13971676, 0.22926724, 0.03219445, -0.04566354, -0.14948955, -0.22817011, -0.08714846, -0.19684613, 0.15479128, 0.2433362) * go_3(0.0, -1.0);\n    result += mat4(0.16050309, -0.102841675, 0.20855242, -0.011171905, -0.10309409, 0.22455123, 0.15892951, -0.06582373, 0.010079549, -0.2055006, -0.09385158, 0.006519388, 0.11838815, 0.37134558, -0.165772, 0.12704434) * go_3(0.0, 0.0);\n    result += mat4(0.11643292, 0.03294274, -0.09800525, -0.13601723, -0.081318736, -0.059975546, -0.039105035, -0.2893635, -0.13024913, -0.058016162, -0.09961072, 0.10532414, 0.24250132, -0.35546342, -0.092634924, 0.093994915) * go_3(0.0, 1.0);\n    result += mat4(-0.18799333, 0.25611782, 0.014645917, -0.063751906, 0.06498416, 0.16619027, -0.14411639, 0.3914421, -0.07343631, -0.116468735, -0.10941946, -0.2553544, -0.37774643, -0.0018441634, 0.06827239, -0.0122299045) * go_3(1.0, -1.0);\n    result += mat4(-0.11884597, -0.2477297, 0.048488285, -0.06438257, -0.124703035, 0.25932777, 0.0650111, -0.0930877, 0.06463341, -0.000544085, 0.0147504965, -0.170097, -0.13241997, 0.20983136, -0.15956205, 0.03424298) * go_3(1.0, 0.0);\n    result += mat4(-0.034574904, 0.06755256, 0.09508443, -0.17162292, 0.046379335, 0.2178781, 0.08699012, -0.055380464, -0.2237568, -0.07427848, -0.028395249, -0.3225617, -0.084454566, -0.24776657, 0.254169, 0.13229847) * go_3(1.0, 1.0);\n    result += vec4(0.18765923, -0.07697714, 0.028134674, -0.060966115);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!SAVE conv2d_7_tf\n//!WIDTH conv2d_6_tf.w\n//!HEIGHT conv2d_6_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.21919365, 0.36627784, 0.12603314, 0.24306288, 0.06447028, 0.06472204, -0.05997039, -0.15651788, 0.017059859, -0.006497198, -0.4189735, 0.021636713, -0.23887977, -0.014220949, 0.031113686, -0.17342716) * go_0(-1.0, -1.0);\n    result += mat4(-0.10818789, -0.03273837, 0.33918005, -0.19290088, 0.0955361, -0.34107623, -0.054906327, -0.18083344, -0.060723677, 0.24395694, 0.112975016, -0.07254578, -0.14389384, 0.13235968, -0.15054801, -0.26216486) * go_0(-1.0, 0.0);\n    result += mat4(-0.23442148, -0.07857079, 0.022283873, -0.2656417, 0.037092753, -0.037313666, -0.5057047, 0.042533103, -0.120424, 0.00021930189, -0.0044566668, -0.45536995, 0.00040759926, 0.14597592, -0.094990164, -0.036161344) * go_0(-1.0, 1.0);\n    result += mat4(0.15024352, 0.19903262, -0.0734784, 0.092836305, -0.025753846, 0.024750374, -0.07550193, 0.035420835, 0.11084378, 0.26119822, -0.08443512, -0.0047807065, -0.042685136, 0.24889739, 0.098650105, 0.2088369) * go_0(0.0, -1.0);\n    result += mat4(-0.25551823, 0.14455976, 0.19886157, -0.23465924, 0.20711218, -0.20875362, -0.11320392, -0.30852005, -0.06795657, 0.008670962, 0.30601278, 0.6929064, 0.17079145, 0.15744895, 0.06441601, 0.06514001) * go_0(0.0, 0.0);\n    result += mat4(0.03142604, -0.006410137, -0.023654792, -0.05708553, 0.062985405, -0.077010594, 0.078804865, 0.050882503, 0.010274228, -0.15558401, 0.09490256, 0.14964707, -0.11966925, -0.36176664, 0.27809814, -0.18862294) * go_0(0.0, 1.0);\n    result += mat4(0.05609992, 0.0041612233, -0.08498908, 0.04479823, -0.080117956, -0.17423204, -0.22858045, 0.054569032, -0.050866384, -0.020000307, 0.027000953, -0.67724514, 0.16240878, -0.04641204, 0.0648367, -0.20613132) * go_0(1.0, -1.0);\n    result += mat4(0.08542306, -0.08254248, -0.11090553, -0.14140448, -0.10788511, -0.13011602, -0.29319742, -0.26007155, 0.11033401, -0.31966573, 0.32668245, 0.19542319, 0.06329418, 0.20904626, 0.2724067, -0.009155685) * go_0(1.0, 0.0);\n    result += mat4(-0.007403411, 0.0012836396, -0.23446666, -0.03017208, 0.062420018, -0.13611084, -0.2975928, 0.13173148, -0.03679939, 0.13743873, -0.10121899, 0.074514665, 0.1497629, -0.09523838, 0.39018926, 0.37807035) * go_0(1.0, 1.0);\n    result += mat4(0.11441487, -0.19565523, -0.25757137, -0.16148767, 0.15575317, -0.12657928, 0.10479676, 0.062919036, 0.010544159, 0.22931573, 0.20360178, 0.4637635, -0.3395036, -0.52467215, 0.08759308, 0.028030418) * go_1(-1.0, -1.0);\n    result += mat4(0.2699195, -0.34218305, 0.15259695, 0.03139074, -0.024053533, -0.029567484, 0.28480124, 0.20525953, 0.15452823, -0.217713, 0.15861876, -0.012275699, 0.21408023, 0.097508304, -0.57126766, -0.14679857) * go_1(-1.0, 0.0);\n    result += mat4(-0.0755847, -0.09751562, -0.29480466, -0.22285318, 0.14196442, 0.114573136, -0.22294767, 0.12463806, 0.3322209, -0.04631724, -0.11097061, -0.27986854, -0.16099304, -0.060079545, 0.00299308, 0.120776065) * go_1(-1.0, 1.0);\n    result += mat4(0.050933484, -0.13776319, -0.18809728, 0.24035202, -0.32528606, -0.41684148, -0.029342847, 0.28642926, -0.07963454, -0.12905268, 0.07606093, 0.24670005, -0.08815598, -0.23320907, -0.008099349, 0.21512873) * go_1(0.0, -1.0);\n    result += mat4(0.19247563, 0.18083979, -0.09719762, 0.15314941, -0.22350982, 0.46515045, -0.3571128, 0.35953265, 0.06921985, -0.4482386, -0.18732521, -0.5043983, 0.35159567, -0.33315298, -0.21884166, -0.16283798) * go_1(0.0, 0.0);\n    result += mat4(-0.021124054, -0.007966742, 0.0052493825, 0.022550896, 0.030403977, 0.3377868, -0.47602004, -0.077664234, -0.07222509, -0.07486097, -0.37971064, -0.5107857, -0.06299477, 0.04930232, -0.3330487, 0.29845512) * go_1(0.0, 1.0);\n    result += mat4(-0.063705474, -0.07917637, -0.02026607, -0.05142568, 0.021577014, -0.07379867, 0.033937998, 0.08148773, -0.02717838, -0.03233838, 0.098000035, 0.036476444, -0.13366953, 0.014477577, 0.24064232, 0.39313284) * go_1(1.0, -1.0);\n    result += mat4(-0.16046515, -0.094624564, 0.35435164, 0.09942324, -0.07137174, -0.27999225, 0.124644354, -0.0062176553, 0.015016751, -0.05500243, -0.23249559, -0.4508382, 0.1860433, 0.10671491, -0.033345353, -0.06611453) * go_1(1.0, 0.0);\n    result += mat4(0.21614046, -0.01307525, -0.18941112, -0.20533535, -0.14481686, -0.47801897, 0.22605121, -0.20298961, -0.06744227, -0.20377496, -0.11926173, 0.15645133, -0.31570885, -0.3495616, -0.024666889, 0.040965475) * go_1(1.0, 1.0);\n    result += mat4(-0.11748018, -0.039976366, -0.00084064255, -0.028653437, -0.16216733, -0.036768105, 0.018064514, -0.0928936, 0.14008482, -0.064511225, 0.24329947, -0.0268608, 0.050330248, 0.08540601, -0.07272679, -0.01187671) * go_2(-1.0, -1.0);\n    result += mat4(-0.09459936, -0.011723822, -0.06952858, -0.07808506, -0.065588176, 0.332501, -0.0120042395, 0.07668016, 0.14735217, -0.14856043, -0.06702449, -0.020953184, -0.023006834, 0.06135422, 0.1491448, -0.028061569) * go_2(-1.0, 0.0);\n    result += mat4(0.25136968, 0.25146323, -0.108277924, -0.20407207, -0.0013780294, 0.16108194, 0.25143847, 0.06672421, -0.033905584, -0.021144686, -0.019152718, 0.34619498, 0.14560962, 0.034437314, 0.024790365, -0.049976267) * go_2(-1.0, 1.0);\n    result += mat4(-0.24928351, 0.12637813, 0.23609994, 0.12722939, -0.036997862, -0.16554876, 0.11144095, -0.10040036, -0.020359103, -0.080701865, -0.3142192, 0.27257237, 0.13546956, -0.14416885, 0.028196262, -0.2886465) * go_2(0.0, -1.0);\n    result += mat4(0.28524777, -0.4236231, 0.27420738, -0.21095508, 0.23475651, 0.115876295, -0.18837357, -0.0260708, 0.030670704, -0.11516913, -0.11365572, -0.2203149, -0.018612983, -0.10719593, -0.031727783, 0.1403327) * go_2(0.0, 0.0);\n    result += mat4(0.07240512, 0.03139215, 0.12328737, -0.021201206, -0.13971715, 0.072742075, -0.0011289873, 0.0053133667, 0.035639685, -0.04322272, -0.19288473, -0.15812221, -0.19126481, 0.0698514, 0.17619178, -0.035605464) * go_2(0.0, 1.0);\n    result += mat4(-0.18552057, 0.07259671, 0.011667668, -0.15630563, 0.11414356, 0.14482655, -0.04021029, 0.18495587, -0.11386139, -0.09058561, -0.011265998, 0.23358451, 0.0521358, 0.12495261, 0.021644838, -0.048094347) * go_2(1.0, -1.0);\n    result += mat4(-0.09222373, 0.0533347, 0.055820454, 0.22382596, 0.18713981, 0.2668916, -0.019384036, 0.012698582, 0.13325234, 0.20361474, -0.33106443, -0.08571572, -0.21243028, -0.10996386, 0.123459645, 0.1534967) * go_2(1.0, 0.0);\n    result += mat4(0.18133277, 0.18108074, -0.05638664, 0.29533157, -0.2108019, -0.033636626, 0.5015888, -0.15116066, -0.041320793, -0.14764231, 0.07314567, -0.18865979, 0.10276937, 0.094240844, -0.1364283, 0.27812913) * go_2(1.0, 1.0);\n    result += mat4(0.06040915, 0.23753685, 0.19019844, 0.23948252, -0.07535012, 0.11848904, 0.14389765, 0.050067905, 0.16150077, -0.030053454, 0.12478255, 0.26020208, 0.111198805, 0.06787492, -0.12771018, 0.006687384) * go_3(-1.0, -1.0);\n    result += mat4(-0.5421617, 0.10414128, -0.21526064, -0.08883624, 0.13145073, -0.29695904, 0.57386386, 0.073361695, -0.09538372, 0.27593842, 0.070922814, 0.21769938, 0.06214975, 0.11847816, 0.10033405, 0.29360098) * go_3(-1.0, 0.0);\n    result += mat4(-0.16294672, -0.014815565, 0.22046989, 0.16858687, 0.058917344, 0.21384977, 0.18803519, 0.105688855, 0.0355118, 0.20571202, -0.07341922, 0.26624045, -0.0415102, 0.050942056, 0.19727907, 0.20122413) * go_3(-1.0, 1.0);\n    result += mat4(-0.020470422, 0.15815964, -0.13437317, -0.1967045, 0.074902646, 0.08356444, 0.055913117, -0.12837863, -0.18647918, 0.07002247, 0.038864706, -0.07288784, 0.04135125, -0.016055549, -0.1340297, -0.15578008) * go_3(0.0, -1.0);\n    result += mat4(-0.07685624, 0.00079105416, -0.068755336, 0.110282525, -0.014170752, 0.041282844, -0.17035173, 0.19439398, -0.3036256, 0.024148455, -0.19566648, -0.06736254, 0.14203559, -0.13016985, -0.32845357, -0.14266774) * go_3(0.0, 0.0);\n    result += mat4(0.0087252045, 0.098839566, -0.08770506, -0.08499465, 0.015245115, -0.110854514, 0.054458305, -0.018121868, -0.09666134, -0.08316006, 0.24617113, -0.17195955, 0.2574254, 0.06734342, -0.13792352, -0.07306126) * go_3(0.0, 1.0);\n    result += mat4(-0.0073954533, -0.20126835, -0.22545357, -0.29462856, 0.057408337, 0.11939119, -0.01846476, 0.12534486, 0.15751605, -0.14282645, -0.14219986, 0.14283386, 0.14090413, 0.10500912, 0.03039335, 0.17448832) * go_3(1.0, -1.0);\n    result += mat4(0.043910783, -0.09140025, -0.21666165, 0.07616939, 0.104454786, 0.309926, -0.12906921, 0.1140117, 0.09372434, 0.049547072, -0.086615674, -0.034449168, 0.096705064, 0.26001686, 0.027063297, 0.12422948) * go_3(1.0, 0.0);\n    result += mat4(0.1365422, 0.2679611, 0.12037257, 0.43346113, 0.08223084, -0.016788265, 0.13570398, -0.017974345, -0.17922844, -0.09475725, 0.073539585, -0.106947675, 0.08998511, 0.04133868, 0.16586913, -0.26291734) * go_3(1.0, 1.0);\n    result += vec4(-0.19233678, 0.016725872, -0.008011114, -0.1977463);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!SAVE conv2d_7_tf1\n//!WIDTH conv2d_6_tf.w\n//!HEIGHT conv2d_6_tf.h\n//!COMPONENTS 4\n#define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.36016628, 0.019064043, 0.3073228, 0.16891135, 0.026739368, 0.31136194, 0.11260383, -0.26918694, 0.0419928, -0.3365078, 0.20189743, -0.04136312, 0.039564647, 0.033199426, 0.18768296, -0.017119858) * go_0(-1.0, -1.0);\n    result += mat4(0.28663483, -0.41716507, 0.059281543, 0.043736435, 0.0028875466, 0.13817391, -0.12543318, -0.2794053, -0.023528943, 0.10610115, 0.09100278, 0.040132936, -0.21949205, -0.027810011, -0.0301218, 0.084047124) * go_0(-1.0, 0.0);\n    result += mat4(0.39674807, -0.0040878756, -0.038235947, 0.11880838, 0.009898328, 0.19107847, -0.009313831, -0.1554276, -0.047341663, 0.18049581, -0.029317195, 0.0708909, 0.0708316, -0.110617444, 0.14584038, -0.022261223) * go_0(-1.0, 1.0);\n    result += mat4(-0.20400241, 0.0896492, -0.010386381, -0.052133385, 0.005023956, -0.06628705, -0.16436209, -0.25345984, -0.05285192, 0.09706557, -0.03778914, -0.152546, 0.17023252, 0.063713826, 0.00743037, 0.056634087) * go_0(0.0, -1.0);\n    result += mat4(-0.080793336, 0.4204207, 0.19098237, 0.20028038, -0.054076545, 0.22064368, -0.25853387, -0.3643562, 0.2085573, -0.023731, -0.06727709, -0.18683033, -0.18032159, -0.06388348, 0.304463, -0.2517781) * go_0(0.0, 0.0);\n    result += mat4(0.11940941, 0.10624008, 0.16120581, 0.2369602, 0.3321827, 0.4272075, -0.10403669, -0.31388018, -0.006372124, -0.00653671, 0.109810196, 0.2277172, 0.005771998, 0.086026914, -0.08934813, -0.094941735) * go_0(0.0, 1.0);\n    result += mat4(-0.13233568, 0.24112508, -0.0068006413, 0.12466225, 0.11396591, -0.07249253, -0.29090378, -0.12828146, -0.22001141, -0.08532405, -0.11932601, 0.29452974, 0.09572195, 0.017603843, 0.12454017, 0.16321751) * go_0(1.0, -1.0);\n    result += mat4(0.042107448, -0.00807216, 0.06580674, -0.1289527, 0.13977426, -0.037159685, -0.21001346, -0.08698161, 0.22370502, -0.29170328, 0.2179206, 0.36621302, 0.0825477, -0.016513655, -0.11157249, 0.12861598) * go_0(1.0, 0.0);\n    result += mat4(0.2246826, -0.13262233, 0.12131653, -0.15522355, 0.38104856, 0.030237729, 0.1286289, -0.19770473, -0.16175011, -0.13688888, 0.23505463, 0.21333031, 0.76352316, -0.17949077, -0.13124311, 0.1613879) * go_0(1.0, 1.0);\n    result += mat4(-0.050607495, 0.0846705, -0.06136092, -0.033436477, 0.41138348, 0.037043408, -0.02676336, -0.37771952, 0.22147503, 0.06490757, -0.04266158, -0.22606373, 0.045775007, -0.054498192, -0.21495876, -0.036050417) * go_1(-1.0, -1.0);\n    result += mat4(-0.06242522, 0.2700824, -0.05602621, -0.12361551, 0.14477442, 0.19403581, 0.23505251, -0.072234035, -0.15831544, 0.4640447, -0.104754634, -0.004539681, -0.20246096, 0.23216484, -0.35886365, 0.11360777) * go_1(-1.0, 0.0);\n    result += mat4(0.14777757, 0.18951412, 0.027219458, 0.11216015, 0.02997997, -0.13466355, -0.0010830094, 0.021302953, 0.23441231, -0.14529245, 0.08068729, 0.10044398, 0.3972878, 0.26570204, 0.0046810666, -0.2863261) * go_1(-1.0, 1.0);\n    result += mat4(-0.10385485, 0.1053724, 0.16961229, 0.20727012, -0.025148917, -0.011365095, 0.03899919, -0.030950211, 0.079080455, -0.32767853, 0.064670205, -0.035771385, 0.16833797, -0.21567492, 0.30871257, -0.19965471) * go_1(0.0, -1.0);\n    result += mat4(-0.23420888, -0.004894698, -0.18162623, -0.31107524, 0.11976508, 0.14924951, -0.08723316, 0.21401922, -0.58200324, -0.01177345, -0.049033508, 0.19593577, -0.21139073, 0.13016601, 0.08734843, 0.4158892) * go_1(0.0, 0.0);\n    result += mat4(0.0009789813, 0.33274913, 0.017405733, -0.042906318, -0.26410276, -0.09291333, 0.019387102, 0.105381854, -0.009176527, 0.09483514, -0.28462934, -0.03644404, 0.285194, -0.4260311, 0.14902237, -0.115670316) * go_1(0.0, 1.0);\n    result += mat4(-0.09344311, 0.4463103, 0.19984834, -0.09733857, -0.118717775, -0.0708026, 0.24919955, -0.11234634, 0.1246395, -0.052909933, 0.1525815, 0.07724016, 0.0070534665, -0.06404165, -0.18149726, -0.014058336) * go_1(1.0, -1.0);\n    result += mat4(-0.17353044, 0.15376104, 0.004588994, -0.13554202, -0.19920237, -0.18918681, 0.11327512, -0.117296435, -0.0785251, 0.013677155, -0.2103214, 0.06843426, -0.27790928, 0.09837545, -0.00019213746, 0.09132539) * go_1(1.0, 0.0);\n    result += mat4(-0.01586651, 0.014929441, 0.2426186, -0.1889374, -0.0865462, -0.07454513, -0.20797268, -0.22366855, 0.19704159, 0.0048206006, -0.16707218, -0.14162683, 0.036798395, -0.1663155, -0.12009389, 0.09603803) * go_1(1.0, 1.0);\n    result += mat4(-0.041532192, 0.05753804, 0.17927068, -0.042112097, 0.12080969, -0.15052572, -0.34855765, -0.07356988, -0.28199884, -0.18958664, 0.15879883, 0.08511588, 0.0034213227, -0.05338495, -0.37285298, 0.06626709) * go_2(-1.0, -1.0);\n    result += mat4(-0.20219134, 0.22150375, -0.29405454, 0.06597703, -0.018885285, -0.010551704, -0.010774283, 0.08758955, -0.2015349, -0.17006227, -0.24321876, -0.06864207, -0.118437864, -0.043977212, -0.029736811, 0.14040919) * go_2(-1.0, 0.0);\n    result += mat4(-0.18709077, -0.09723938, 0.12783436, -0.15167634, 0.29039705, -0.11009911, 0.018371418, -0.060096707, -0.07256923, -0.25799567, -0.06276934, -0.035992302, -0.06729111, -0.059956793, -0.024079734, 0.011838878) * go_2(-1.0, 1.0);\n    result += mat4(0.010449175, -0.08212451, 0.1409803, 0.11861122, -0.18035835, 0.051930565, 0.01049551, -0.09447962, 0.12029649, 0.040604513, -0.059971705, -0.0044667358, -0.22080486, -0.11187681, 0.124374695, -0.004155485) * go_2(0.0, -1.0);\n    result += mat4(-0.28584236, -0.38480133, -0.13987814, -0.4463469, -0.3890419, -0.022498172, 0.17334452, 0.21895568, -0.15450422, -0.10905497, 0.15111905, -0.22554915, 0.106121585, -0.029144369, 0.36059046, 0.22140682) * go_2(0.0, 0.0);\n    result += mat4(-0.23780307, -0.023033705, 0.068205886, -0.110635854, -0.26720005, -0.1608183, 0.19523881, 0.07972837, -0.018495852, -0.2793956, 0.17668398, -0.12020479, -0.079556085, -0.02284952, 0.031480275, 0.31818348) * go_2(0.0, 1.0);\n    result += mat4(0.22501226, -0.00829407, 0.059581667, 0.16512989, 0.18711442, 0.1200968, 0.11812652, -0.16091056, 0.15733972, 0.045156084, 0.20640492, -0.16852027, -0.11217177, 0.06746273, -0.050218176, 0.08643783) * go_2(1.0, -1.0);\n    result += mat4(0.20715691, -0.1082907, 0.027892975, 0.19515261, -0.17838904, 0.1532257, -0.108409844, -0.06632365, -0.13805026, 0.23020233, 0.12416581, -0.14861803, 0.16650471, 0.08158386, -0.09051303, -0.06981649) * go_2(1.0, 0.0);\n    result += mat4(-0.04617126, 0.06579221, 0.25964734, 0.28500968, 0.07641255, -0.090885855, -0.0972522, 0.18298368, -0.06393334, 0.103463, -0.23062052, -0.15270731, 0.13633437, 0.074707486, 0.15065335, -0.024602572) * go_2(1.0, 1.0);\n    result += mat4(0.118319295, 0.010410938, 0.044655934, -0.104725905, 0.030477569, 0.12867387, 0.039075315, 0.18922117, 0.13301082, -0.1601557, 0.038168408, -0.07372259, -0.09522213, -0.095107146, -0.16679631, 0.044673234) * go_3(-1.0, -1.0);\n    result += mat4(0.46229, -0.30780822, -0.09081465, 0.1433387, -0.0315039, 0.059409115, -0.24948491, -0.17146957, 0.060843736, -0.041989822, 0.054005735, 0.22835566, 0.12036598, -0.0070898845, 0.17276852, -0.17754094) * go_3(-1.0, 0.0);\n    result += mat4(-0.35119572, 0.020034311, 0.08751943, 0.08193488, 0.041884877, 0.22649358, -0.07447533, 0.20845473, -0.04859846, -0.16206735, 0.06819576, -0.053000778, 0.18146423, 0.04694148, 0.045293212, 0.06783575) * go_3(-1.0, 1.0);\n    result += mat4(0.280914, -0.14998704, -0.23485807, -0.015608296, 0.1549556, -0.11992663, -0.094974115, 0.05887284, 0.053392075, 0.10322464, -0.075066686, 0.068358354, -0.18663338, 0.009901499, -0.123370335, -0.12502703) * go_3(0.0, -1.0);\n    result += mat4(0.7748568, -0.17870626, -0.20770052, 0.024692526, -0.056430295, -0.06324113, -0.03660047, 0.29629672, -0.51896983, -0.027231261, 0.05903762, 0.077677645, -0.061675485, -0.20277846, 0.10352223, -0.08198446) * go_3(0.0, 0.0);\n    result += mat4(-0.06347568, 0.21643166, -0.09718546, 0.0372257, -0.029537952, -0.0357135, -0.09548363, 0.18225233, -0.29609334, -0.3496132, 0.18245913, -0.10162589, -0.18189451, -0.09077887, 0.117313184, -0.06863874) * go_3(0.0, 1.0);\n    result += mat4(-0.047373574, -0.020289376, -0.25748715, -0.13568166, 0.15656634, -0.06841899, 0.012100781, -0.13611819, 0.0016357322, -0.23870537, 0.14035743, -0.14700134, 0.2535575, -0.13697346, -0.13693139, -0.10365287) * go_3(1.0, -1.0);\n    result += mat4(0.4283934, -0.316192, -0.012617617, 0.018468965, 0.21436644, 0.18408814, -0.42651537, 0.12504087, -0.13894933, 0.091662176, -0.20096369, -0.080727175, -0.005487846, 0.17046383, 0.1383948, -0.0054956395) * go_3(1.0, 0.0);\n    result += mat4(0.20014295, -0.027282396, -0.06317007, 0.04452042, 0.064600386, 0.072222926, -0.33409226, 0.08063831, -0.022607977, 0.1308856, -0.39691743, -0.094889864, -0.1810531, 0.011367248, -0.2531222, -0.22468317) * go_3(1.0, 1.0);\n    result += vec4(0.26886886, 0.05874665, 0.10268232, 0.05833081);\n    return result;\n}\n//!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-3x1x1x112\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!BIND conv2d_7_tf\n//!BIND conv2d_7_tf1\n//!SAVE MAIN\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n#define g_0 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_1 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_2 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_5 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_6 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_8 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_9 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_10 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_12 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_13 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_14 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_15 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_16 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_17 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_18 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_19 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_20 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_21 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\n#define g_22 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_23 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\n#define g_24 (max((conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0))\n#define g_25 (max((conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0))\n#define g_26 (max(-(conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0))\n#define g_27 (max(-(conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.09689336, 0.06046458, 0.072598994, 0.0, 0.11994565, 0.104477674, 0.09302802, 0.0, -0.05718302, 0.050438102, 0.08814741, 0.0, 0.0308889, 0.0033925986, -0.01715605, 0.0) * g_0;\n    result += mat4(-0.028314235, 0.06597744, 0.0966897, 0.0, 0.035656154, 0.07770106, 0.075551905, 0.0, 0.0001793458, -0.000479495, -0.00297406, 0.0, -0.053916585, -0.016807461, -0.0057141334, 0.0) * g_1;\n    result += mat4(-0.047189303, -0.0207, -0.020910334, 0.0, -0.07933196, -0.06961211, -0.086069845, 0.0, 0.0943727, 0.008463375, 0.010755166, 0.0, 0.062410597, 0.022625161, 0.04068433, 0.0) * g_2;\n    result += mat4(0.10270994, -0.019080428, 0.0050091282, 0.0, -0.004672948, -0.013966742, -0.0063746064, 0.0, -2.5856789e-05, 0.03151499, -0.0023983798, 0.0, 0.113539025, 0.12381699, 0.100360274, 0.0) * g_3;\n    result += mat4(0.07868885, -0.030913834, -0.009213676, 0.0, 0.04870991, 0.021467991, 0.038739506, 0.0, -0.042969644, -0.07122453, -0.08798675, 0.0, -0.09784122, 0.021434791, 0.02510374, 0.0) * g_4;\n    result += mat4(0.050420716, 0.0729716, 0.076532185, 0.0, -0.019112485, -0.01037939, -0.026948035, 0.0, -0.02591423, 0.008927897, -0.00042541025, 0.0, 0.1043701, -0.0071186824, -0.041817162, 0.0) * g_5;\n    result += mat4(-0.16143242, -0.0009298223, -0.01228508, 0.0, 0.07744052, -0.018313263, -0.0488145, 0.0, 0.09241393, 0.07128674, 0.055164956, 0.0, 0.054884013, -0.04834418, -0.06281626, 0.0) * g_6;\n    result += mat4(-0.049036566, -0.05979936, -0.05594288, 0.0, -0.014564307, 0.031926468, 0.037857566, 0.0, 0.015474487, -0.11385003, -0.11527764, 0.0, -0.07076006, 0.057038613, 0.095983796, 0.0) * g_7;\n    result += mat4(0.03094887, -0.008734403, 0.00042712069, 0.0, 0.053891554, 0.05837673, 0.06200635, 0.0, 0.09071558, -0.04202184, -0.046172567, 0.0, -0.0425916, 0.04905093, 0.020835675, 0.0) * g_8;\n    result += mat4(0.096628904, -0.037792254, -0.043241944, 0.0, -0.011923947, -0.025950424, -0.031381752, 0.0, -0.060941868, -0.07859433, -0.07535451, 0.0, -0.026777223, 0.08604982, 0.07829908, 0.0) * g_9;\n    result += mat4(-0.06435972, 0.0036599538, 0.00786578, 0.0, -0.061972067, -0.05681472, -0.06667608, 0.0, -0.106890626, 0.007406496, 0.029977169, 0.0, -0.20519382, -0.044860814, 0.0021225857, 0.0) * g_10;\n    result += mat4(-0.16876474, 0.012789643, 0.026692612, 0.0, 0.017817136, 0.026935097, 0.02227043, 0.0, 0.01690181, 0.07716103, 0.086527, 0.0, 0.07923805, -0.10443151, -0.10859543, 0.0) * g_11;\n    result += mat4(0.003730466, -0.024648283, -0.022169832, 0.0, -0.0062762927, 0.022062732, 0.032966793, 0.0, 0.016349113, 0.017197203, 0.020952817, 0.0, -0.1763789, 0.035497356, 0.053835396, 0.0) * g_12;\n    result += mat4(0.020886675, -0.07054202, -0.079142675, 0.0, 0.06664387, 0.044960167, 0.042230908, 0.0, -0.095019594, 0.012421141, 0.0142890485, 0.0, 0.056814816, -0.012751135, -0.014684506, 0.0) * g_13;\n    result += mat4(0.011765893, 0.0008920681, -0.0018258415, 0.0, -0.010473814, -0.023085753, -0.028783914, 0.0, -0.023034256, -0.0024786016, -0.0052162083, 0.0, 0.1643386, -0.06132718, -0.09289065, 0.0) * g_14;\n    result += mat4(0.016597198, 0.09389637, 0.10833379, 0.0, -0.043163072, -0.04714812, -0.035274632, 0.0, 0.09634976, -0.009292612, -0.022424143, 0.0, -0.08765172, 0.0051558353, 0.010900356, 0.0) * g_15;\n    result += mat4(0.030815786, 0.021069322, 0.01812191, 0.0, 0.084839165, -0.0080813095, -0.029270556, 0.0, -0.10456346, 0.062386703, 0.0665605, 0.0, 0.11926609, -0.1104228, -0.13291118, 0.0) * g_16;\n    result += mat4(-0.07159541, -0.007267032, -0.010134558, 0.0, 0.008234213, 0.045609634, 0.040295456, 0.0, 0.018416971, 0.01308482, 0.014649557, 0.0, 0.035107512, -0.02140815, -0.030279048, 0.0) * g_17;\n    result += mat4(0.01918586, 0.03875863, 0.03229402, 0.0, -0.07917104, 0.041135103, 0.057182517, 0.0, 0.08609541, 0.0079662455, 0.004327576, 0.0, -0.14332893, 0.03120354, 0.056732506, 0.0) * g_18;\n    result += mat4(0.03200192, -0.0035752193, -0.0031064528, 0.0, -0.010902813, 0.014607456, 0.019431474, 0.0, -0.016461229, -0.004938204, -0.004655488, 0.0, -0.033470232, 0.0026075812, 0.005896968, 0.0) * g_19;\n    result += mat4(0.037410006, 0.048742272, 0.04348088, 0.0, 0.037719514, 0.030768529, 0.03127472, 0.0, 0.056426726, 0.03066893, 0.016440205, 0.0, -0.010599352, 0.022832409, 0.023211194, 0.0) * g_20;\n    result += mat4(-0.005733291, 0.06365659, 0.06663611, 0.0, -0.041917093, -0.016493445, -0.020438088, 0.0, -0.0014357592, -0.0022506563, -0.0045095007, 0.0, 0.029893145, -0.009129354, -0.015173116, 0.0) * g_21;\n    result += mat4(0.013052085, 0.005108175, 0.0025906067, 0.0, -0.021950055, -0.036447693, -0.036141638, 0.0, -0.036296472, 0.0068928464, 0.013102313, 0.0, 0.0060471976, -0.024798103, -0.023548538, 0.0) * g_22;\n    result += mat4(0.0067743887, -0.06191211, -0.062355213, 0.0, 0.0016080744, -0.020445071, -0.016840393, 0.0, 0.028264903, 0.01852915, 0.015891539, 0.0, -0.023877412, -0.013271666, -0.008158679, 0.0) * g_23;\n    result += mat4(-0.04317466, -0.018953001, -0.020452993, 0.0, -0.009322576, -0.03022352, -0.030970376, 0.0, 0.05653658, 0.05430553, 0.046692245, 0.0, 0.05615359, 0.059338935, 0.056018773, 0.0) * g_24;\n    result += mat4(0.022878079, 0.03392234, 0.033057988, 0.0, -0.017554542, -0.0141542535, -0.014122613, 0.0, -0.048634093, -0.05316463, -0.047988772, 0.0, -0.058002178, -0.040221967, -0.034025013, 0.0) * g_25;\n    result += mat4(-0.018253656, -0.04197674, -0.040467236, 0.0, -0.04358929, -0.028309818, -0.025425073, 0.0, -0.008488672, -0.001727991, 0.00035808363, 0.0, -0.0011709273, 0.0052514165, 0.0059479307, 0.0) * g_26;\n    result += mat4(-0.08333935, -0.09818201, -0.09476284, 0.0, -0.033692095, -0.046259012, -0.045797516, 0.0, -0.007577072, 0.0022402718, 0.0016200038, 0.0, 0.0029786075, -0.020728534, -0.018938033, 0.0) * g_27;\n    result += vec4(0.047567394, -0.02504617, -0.028163986, 0.0);\n    return result + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Upscale_CNN_x2_M.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.010995803, 0.077095956, -0.043992598, 0.06048717, 0.1164834, -0.11689607, 0.072985925, -0.078805886, 0.01182932, 0.054985743, -0.09018186, 0.044907484, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(0.1813623, -0.14752422, 0.025720436, -0.17639883, 0.15697388, 0.10445984, -0.1843076, 0.5264643, 0.047516696, -0.097305484, 0.09740847, -0.29619336, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(-0.014534763, 0.09486465, 0.046173926, 0.039391946, 0.09609376, -0.060574662, 0.042200956, -0.3269777, 0.051006425, 0.059818447, 0.04366627, 0.17699827, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(0.04268535, -0.08152529, 0.10577459, -0.036936995, -0.051562306, 0.054872766, 0.09194519, 0.0025066638, -0.01073954, 0.00064474024, 0.10038221, 0.02131141, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.51751363, -0.40028602, 0.3469574, 0.5933738, -0.91357684, -0.67692596, 0.57815677, 0.39809322, -0.16341521, -0.27169713, 0.12232366, 0.4318641, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.12601124, -0.06263236, -0.45907676, -0.41514075, 0.3330334, -0.1929565, -0.6333532, -0.6552794, -0.045809917, 0.046351526, -0.26173338, -0.30252662, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.0030332592, 0.012103107, 0.010537323, -0.02038607, 0.095558085, 0.097704545, 0.083433494, 0.026790185, 0.01943357, -0.061712462, -0.00015703632, -0.032268334, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.016870102, 0.5215812, -0.11525501, 0.027527615, -0.09045733, 0.61310345, -0.1575268, 0.1905386, 0.020172214, 0.3503187, -0.08209157, -0.051328037, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(0.005494087, -0.010656317, 0.07682753, -0.08116042, -0.03934524, 0.16589017, 0.101483546, -0.066603065, 0.03494657, -0.07885597, 0.074227594, 0.0016264897, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(0.014463938, -0.0031906287, 0.007015422, -0.003888468);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.08532478, -0.14302494, -0.017921071, -0.0032664281, -0.09841952, 0.024187077, 0.10701477, 0.14110753, -0.05714981, -0.10897174, 0.073803626, 0.103992954, 0.07914382, 0.032193683, -0.18346278, -0.09723936) * go_0(-1.0, -1.0);\n    result += mat4(-0.034482613, -0.10742312, -0.047286414, -0.08641124, -0.33896688, -0.036533825, -0.48337597, 0.034040943, -0.13598205, -0.080917805, 0.08540263, -0.012667689, -0.009171425, -0.120026454, -0.20536867, -0.032149274) * go_0(-1.0, 0.0);\n    result += mat4(0.18687321, 0.066278316, 0.024327392, 0.08816582, -0.08017908, 0.09488853, 0.26018232, -0.101504356, 0.17487666, 0.31057635, 0.14785016, -0.09622089, -0.07537452, -0.13844088, -0.05810814, 0.09907489) * go_0(-1.0, 1.0);\n    result += mat4(-0.04183032, 0.15207712, 0.005002397, 0.32277516, -0.16169126, -0.119836345, -0.04068436, -0.096728764, 0.11943901, 0.1789597, -0.20412198, 0.19009817, 0.36630696, 0.06946421, -0.5254373, -0.11896399) * go_0(0.0, -1.0);\n    result += mat4(-0.31916487, -0.98911583, 1.0728644, -0.39280394, 0.33458877, -0.17325239, -0.645045, -0.28524077, -0.14512783, 0.24996442, -0.09837877, 0.05468934, 0.31559715, -0.020504637, -0.026724018, 0.24507573) * go_0(0.0, 0.0);\n    result += mat4(-0.23759829, -0.08530173, -0.16665787, -0.22463752, 0.109896734, 0.13446991, -0.049552456, -0.02385489, -0.01245375, 0.3833208, 0.05758832, 0.1528937, 0.0501858, -0.19651426, 0.0076587177, -0.03297025) * go_0(0.0, 1.0);\n    result += mat4(0.14554465, -0.01826686, 0.10284085, -0.19152659, -0.017585073, -0.05511482, 0.06362406, 0.023924058, -0.0018977845, -0.103172876, 0.03287086, -0.20085956, 0.36062446, 0.10749464, -0.20984372, 0.018256644) * go_0(1.0, -1.0);\n    result += mat4(-0.005534592, 0.3709197, -0.18287498, 0.1720451, 0.030155553, -0.023265475, 0.0058617783, -0.031765483, 0.037328955, -0.2730994, 0.35090837, -0.3269043, -0.028477207, 0.32756507, -0.15989502, 0.12158258) * go_0(1.0, 0.0);\n    result += mat4(0.10873739, 0.19583772, 0.060394943, 0.09410379, -0.04739245, 0.026561242, 0.022990001, 0.1093272, -0.01071349, -0.022938967, -0.046423864, 0.2385325, -0.0319821, 0.046962265, 0.09081178, -0.11001857) * go_0(1.0, 1.0);\n    result += mat4(0.13012704, 0.112289295, 0.030790284, -0.050499484, 0.11784853, 0.08107028, -0.07556717, -0.15643, 0.015249331, 0.015299608, 0.07748125, 0.054485757, 0.044857923, 0.12161275, -0.048292994, -0.033995003) * go_1(-1.0, -1.0);\n    result += mat4(0.12931514, 0.15114146, 0.070513315, 0.11246343, 0.4142387, 0.213479, -0.5439916, 0.07776645, 0.13109331, 0.2021147, 0.25932786, -0.22157331, 0.02377734, -0.014970623, -0.1943276, 0.18440372) * go_1(-1.0, 0.0);\n    result += mat4(-0.22365458, -0.19829084, -0.06881161, -0.06468993, 0.17202774, 0.0048758537, -0.09235021, 0.18941896, 0.064125344, -0.09067088, 0.09748182, 0.13561936, -0.05876288, -0.0122420965, -0.054380875, -0.17743628) * go_1(-1.0, 1.0);\n    result += mat4(0.18582906, -0.09263032, -0.08210888, -0.20515606, 0.11484005, 0.08557595, 0.0009253741, -0.051202174, -0.18535301, -0.1529345, -0.13092944, 0.03770747, -0.020947013, 0.19187425, -0.15494856, -0.048979875) * go_1(0.0, -1.0);\n    result += mat4(-0.38131633, 0.4278787, 0.19763695, 0.27655518, -0.08711912, 0.07374453, -0.064803004, 0.5983854, 0.2361923, -0.057221692, -0.37138999, -0.24259573, 0.13890724, 0.25706333, -0.54021406, 0.08095518) * go_1(0.0, 0.0);\n    result += mat4(0.0991328, -0.022651536, -0.029148921, -0.009812537, -0.09523686, -0.15704902, 0.052389514, 0.21561539, 0.1950314, -0.08572602, 0.0016523858, 0.14125621, -0.030999828, 0.12009709, 0.0373512, -0.105043754) * go_1(0.0, 1.0);\n    result += mat4(-0.11251988, 0.12106985, 0.011923068, 0.3662747, 0.004800994, 0.017972551, 0.004761366, -0.07934206, -0.13755941, -0.022852683, 0.1502225, 0.009758547, -0.16964264, 0.00984782, 0.07855833, 0.035730787) * go_1(1.0, -1.0);\n    result += mat4(0.01964957, -0.27226487, 0.033933397, -0.117632054, -0.009058229, 0.047830686, -0.01125145, 0.136628, 0.0056388285, 0.3028781, -0.12286517, 0.23498532, -0.009319075, -0.444048, 0.16174883, -0.06367683) * go_1(1.0, 0.0);\n    result += mat4(0.02343933, -0.010915871, -0.058680378, -0.21886891, -0.010750894, -0.06671997, 0.0602906, -0.07903071, 0.066891186, 0.06650588, 0.14362891, -0.101870626, 0.02264628, -0.06940821, -0.077616625, 0.110911585) * go_1(1.0, 1.0);\n    result += vec4(0.032014452, -0.020821465, 0.0826416, -0.002838458);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.06963679, -0.07560548, -0.069522075, 0.0038078027, -0.08002613, 0.13671301, 0.084461786, -0.039376218, 0.19136548, -0.123174496, 0.26566333, -0.16583005, -0.18664864, -0.023539122, -0.21928434, -0.026818147) * go_0(-1.0, -1.0);\n    result += mat4(0.16660932, -0.18558703, 0.37230486, 0.118128106, -0.14098641, 0.14659132, -0.22217897, 0.12952235, -0.4139033, -0.04308319, 0.12885277, -0.17986743, -0.23556231, -0.08351981, -0.43240538, 0.019033253) * go_0(-1.0, 0.0);\n    result += mat4(-0.18008037, -0.04448665, 0.011906908, -0.023056917, 0.18136618, -0.04723555, -0.0050158803, -0.14823224, -0.2105281, 0.023047728, -0.14040631, -0.03178526, -0.13477588, -0.01820428, 0.058358394, 0.23792502) * go_0(-1.0, 1.0);\n    result += mat4(0.07363309, -0.061728477, 0.03573137, -0.0050971056, -0.012813505, -0.17236637, 0.1697835, 0.055788577, -0.22263195, 0.10324512, 0.58971673, -0.4872246, -0.1555681, 0.032747746, -0.096495196, 0.070196226) * go_0(0.0, -1.0);\n    result += mat4(0.14174286, 0.099460006, -0.088765986, 0.58350676, -0.025177564, -0.46004987, 0.37007022, -0.11437029, -0.5164534, -0.60465246, 0.38859612, -0.32846406, 0.050266482, -0.20334712, 0.18316261, -0.19327633) * go_0(0.0, 0.0);\n    result += mat4(-0.09377763, -0.0012762006, -0.028991895, -0.26523829, 0.20173682, 0.037923716, -0.03174243, 0.07103378, -0.10764164, -0.30752546, 0.20556998, -0.1892279, 0.08115748, -0.023550175, -0.07627362, 0.11746628) * go_0(0.0, 1.0);\n    result += mat4(-0.06998859, -0.017997518, 0.069938794, -0.14943017, -0.14179112, 0.16643842, -0.110231474, 0.08895815, -0.24074875, 0.3277253, -0.07435203, -0.23452802, 0.039962552, -0.07145652, -0.022511544, -0.04571222) * go_0(1.0, -1.0);\n    result += mat4(-0.059785757, -0.23771374, -0.030571314, 0.25222278, 0.106601834, 0.34398326, 0.14511436, -0.03867526, -0.38982397, -0.11944689, 0.12997924, -0.13079585, 0.005729482, 0.012653905, -0.063693404, 0.09632285) * go_0(1.0, 0.0);\n    result += mat4(-0.04933823, 0.0547175, 0.050636575, -0.10060694, 0.1344485, 0.19752938, -0.100068115, -0.028829506, -0.14096203, -0.079092234, 0.092109434, 0.011606209, -0.04052607, -0.008347507, 0.06956573, -0.028109524) * go_0(1.0, 1.0);\n    result += mat4(0.21918017, -0.11115073, 0.2262453, -0.06889667, -0.11256312, -0.07438075, -0.088454485, 0.13672407, -0.06905764, 0.08128395, 0.016103368, 0.050190717, 0.09691516, 0.05845721, 0.4886816, 0.041121427) * go_1(-1.0, -1.0);\n    result += mat4(-0.3449472, 0.09711974, -0.13881907, -0.018265123, 0.27855873, -0.07030004, 0.29545054, 0.37216932, 0.08657718, 0.099066615, -0.10574013, -0.17667885, -0.14855732, -0.11351448, 0.66945946, 0.11312157) * go_1(-1.0, 0.0);\n    result += mat4(0.2526151, -0.04594331, -0.06606611, 0.09104881, 0.06857995, -0.075284235, -0.17664689, 0.21578754, 0.0696524, 0.09142951, 0.080997564, -0.0682772, -0.0011445724, -0.11736295, 0.2519232, -0.101926275) * go_1(-1.0, 1.0);\n    result += mat4(-0.12913518, 0.058357026, 0.195421, -0.15651494, 0.2877076, 0.0033844314, -0.07831594, 0.052855384, -0.031295884, 0.03301088, -0.18408822, 0.06732994, 0.23742151, -0.12568143, 0.22810535, -0.11545694) * go_1(0.0, -1.0);\n    result += mat4(-0.49203303, -0.22656603, 0.1723193, -0.51250046, -0.09742038, 0.758559, -0.3387505, -0.6193586, 0.14136684, 0.27679884, -0.050113205, 0.31041816, -0.36475047, -0.48746544, 0.3233227, 0.4579754) * go_1(0.0, 0.0);\n    result += mat4(0.46636763, 0.1507748, -0.2581362, 0.15413165, -0.17160143, 0.14256273, -0.074575804, -0.099299066, -0.0017214464, 0.13778336, -0.07378213, -0.15489665, -0.10533715, -0.0011083825, 0.39584312, 0.0023906573) * go_1(0.0, 1.0);\n    result += mat4(0.026959421, -0.06391859, 0.0034752619, 0.14521928, -0.0010877338, -0.032619733, 0.005375293, -0.018952755, 0.03381545, -0.007652831, 0.034141563, 0.046016496, 0.11219674, 0.030913852, 0.077403754, 0.17192438) * go_1(1.0, -1.0);\n    result += mat4(0.040326044, 0.17290725, -0.1220239, -0.09594783, -0.025229257, 0.17913155, -0.26623353, -0.033396784, -0.03075146, 0.009143897, -0.0136083895, -0.13886899, 0.075683735, -0.11584183, 0.22182357, 0.19350322) * go_1(1.0, 0.0);\n    result += mat4(0.15726025, -0.10215694, -0.060057458, 0.26487043, -0.04075552, -0.016496127, 0.0015382086, 0.108562306, 0.026795091, 0.0441233, -0.08754318, -0.0460157, 0.048422016, 0.14107347, 0.07986661, 0.1047697) * go_1(1.0, 1.0);\n    result += vec4(0.0766796, 0.08115133, -0.05703058, 0.14025708);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!SAVE conv2d_3_tf\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.18038331, 0.21830973, -0.10019419, -0.022745568, -0.14944611, -0.15669158, 0.46361133, -0.07289843, 0.02976627, -0.09000817, 0.113060996, 0.05635241, 0.012762965, -0.022688959, 0.01629751, 0.061114635) * go_0(-1.0, -1.0);\n    result += mat4(0.024338024, -0.10004009, -0.13709056, -0.0851965, 0.23927099, -0.024349794, -0.16574804, 0.084686354, -0.047885604, 0.09688507, -0.12733915, 0.06980246, 0.11480734, 0.014669346, -0.07505829, 0.04676309) * go_0(-1.0, 0.0);\n    result += mat4(0.054203495, 0.011881634, -0.036115017, -0.0686298, -0.13682245, -0.15678032, 0.057050128, -0.03368558, 0.13011025, 0.033391044, -0.09841339, -0.027057761, -0.18701133, 0.20852546, -0.13660902, 0.0005817616) * go_0(-1.0, 1.0);\n    result += mat4(-0.08077834, 0.35952288, -0.07647382, -0.0033230998, 0.13929126, -0.09155619, 0.14128102, 0.16005981, 0.18161216, -0.09485738, 0.0029118075, 0.052682754, 0.03242074, 0.08299826, 0.073796146, -0.06446532) * go_0(0.0, -1.0);\n    result += mat4(-0.36655015, 0.4606936, 0.19073649, 0.31655258, -0.006838053, -0.579939, 0.089126326, -0.14021218, -0.3437716, 0.16714323, 0.17705944, -0.22418492, -0.3883696, -0.2302651, 0.2581861, 0.21983066) * go_0(0.0, 0.0);\n    result += mat4(0.0992383, -0.014257871, -0.023896435, 0.19868234, 0.0408007, 0.07995299, 0.16102871, -0.11668251, 0.22458278, -0.05587917, 0.19373615, -0.016202094, -0.25106144, 0.15634494, 0.11624891, -0.2930768) * go_0(0.0, 1.0);\n    result += mat4(0.024616942, 0.36248252, -0.14779098, -0.019894283, -0.007111256, 0.010641561, -0.09541178, 0.21236233, 0.009501827, 0.08132797, -0.13983901, 0.027207611, 0.038444366, -0.013995817, -0.16242191, 0.03294123) * go_0(1.0, -1.0);\n    result += mat4(0.0131698875, -0.18124102, -0.13503514, -0.06099072, 0.07422735, -0.20906176, -0.049005672, 0.08739405, -0.031758767, -0.1978915, 0.23094437, 0.54512614, 0.21338555, -0.011205669, -0.23727885, -0.29533875) * go_0(1.0, 0.0);\n    result += mat4(-0.0010255767, -0.07168225, -0.033568826, 0.22161655, -0.087293416, 0.11350447, 0.13653576, 0.061226424, -0.13074352, 0.058425818, 0.038460605, 0.2749964, -0.012814839, 0.085885845, -0.038151987, -0.17960808) * go_0(1.0, 1.0);\n    result += mat4(0.19728905, -0.040724937, -0.18270236, 0.046735186, 0.03507326, 0.119867206, -0.12691991, 0.18119748, -0.052895024, 0.11348764, -0.043787055, 0.004703516, 0.006752757, -0.06939761, -0.009801806, -0.075640485) * go_1(-1.0, -1.0);\n    result += mat4(0.051735226, 0.1732299, -0.10672899, 0.0320877, -0.4913656, 0.2102274, 0.43920282, 0.059108034, 0.08349019, -0.16517872, 0.15436842, -0.1075667, 0.022741623, -0.26693836, 0.3645307, 0.017874828) * go_1(-1.0, 0.0);\n    result += mat4(0.034464058, 0.014929155, 0.054227423, 0.14167373, -0.0023630706, -0.08904212, 0.11918041, -0.034539603, 0.06048089, -0.06807333, 0.14447778, 0.035260547, 0.09979546, -0.1924939, 0.14596114, -0.12069667) * go_1(-1.0, 1.0);\n    result += mat4(-0.04427228, -0.23673469, 0.010357103, -0.2907043, -0.06845721, -0.078984015, 0.06867713, -0.058163825, -0.12154615, 0.08430951, 0.1922373, 0.030108064, -0.43081748, -0.38715646, -0.022240646, -0.15403675) * go_1(0.0, -1.0);\n    result += mat4(0.46885306, -0.33421394, -0.6695223, -0.41841158, 0.30317923, 0.24244753, -0.1047785, -0.18656285, 0.06261881, -0.4405616, 0.24233986, 0.40070608, 0.81440526, 0.11305212, -0.8826317, -0.023478031) * go_1(0.0, 0.0);\n    result += mat4(-0.07879348, -0.024378026, -0.041883785, -0.17030984, 0.23229122, -0.011237109, 0.12058088, 0.20766267, -0.36519575, 0.09599417, -0.1271098, 0.06990154, 0.21161246, 0.041002538, -0.36046275, 0.007304667) * go_1(0.0, 1.0);\n    result += mat4(0.10873893, 0.003872542, -0.13476561, -0.036068805, -0.054637462, 0.02304618, 0.04707738, -0.2856381, 0.07124422, 0.010866545, 0.20484549, -0.008342406, -0.43660247, -0.041055538, 0.33536008, -0.060022205) * go_1(1.0, -1.0);\n    result += mat4(0.1966458, 0.0016302796, -0.25712642, -0.09639119, -0.006955351, 0.10882133, 0.1107341, 0.062697805, -0.1074494, 0.17361663, 0.6429869, -0.39846307, -0.26302996, 0.048710946, 0.40387508, 0.4299715) * go_1(1.0, 0.0);\n    result += mat4(0.18948616, 0.24086732, -0.064474985, -0.11069709, 0.1279659, -0.13438123, -0.028438117, 0.125883, 0.018153818, -0.21942288, 0.020390838, -0.22797634, -0.10821287, -0.17175092, 0.122016855, 0.20699544) * go_1(1.0, 1.0);\n    result += vec4(-0.05101961, -0.060740646, -0.024465766, 0.058471628);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!SAVE conv2d_4_tf\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.14533128, 0.07266841, 0.13238011, -0.23328504, 0.031516243, 0.058471266, -0.06394412, 0.090752736, -0.0042359144, 0.12357294, -0.04377495, 0.0011743477, 0.05412243, -0.08146249, 0.04002749, -0.032876283) * go_0(-1.0, -1.0);\n    result += mat4(-0.036972385, -0.15238069, -0.3453321, -0.36025128, 0.07597202, -0.02368151, -0.3889606, 0.34607083, 0.3133179, -0.21712309, -0.4210954, 0.21450534, 0.15226828, 0.25326282, 0.45327064, -0.3350824) * go_0(-1.0, 0.0);\n    result += mat4(0.019018406, -0.33060563, -0.092601225, 0.14970545, 0.1441509, -0.19228427, -0.032771986, 0.26331595, 0.052981265, -0.06627376, -0.08634131, 0.038706224, 0.13403937, -4.4842476e-05, 0.049002815, -0.12719193) * go_0(-1.0, 1.0);\n    result += mat4(0.17527401, -0.0035254909, -0.047959115, -0.4526988, -0.07510284, 0.0013256798, -0.07539148, 0.24220634, -0.08708839, -0.14494033, -0.17085724, -0.099797316, 0.0068515535, -0.08918779, 0.27164719, -0.1702649) * go_0(0.0, -1.0);\n    result += mat4(0.31848368, 0.48983255, -0.44140294, -0.65174145, -0.004199057, 0.19494705, 0.5196497, -0.027118586, 0.032509074, -0.23900363, -0.14489244, 0.36314297, -0.23168536, -0.20960593, 0.61471456, 0.12401275) * go_0(0.0, 0.0);\n    result += mat4(-0.24317405, 0.21560913, 0.15564032, 0.11606844, -0.15039803, -0.59578896, 0.14100945, -0.026194477, 0.37237462, -0.49472088, -0.15215331, -0.38820064, -0.25089455, -0.29643852, -0.09513793, 0.019779462) * go_0(0.0, 1.0);\n    result += mat4(0.12498539, 0.0710632, -0.25012368, -0.2272255, -0.08647026, 0.12277892, 0.011025097, -0.12168395, -0.13489573, 0.016708186, -0.15583871, -0.057124946, 0.1216943, 0.019803725, 0.06952334, -0.032985855) * go_0(1.0, -1.0);\n    result += mat4(0.28794885, 0.33783793, -0.14469545, -0.081780486, -0.50320613, -0.067601606, -0.06847453, -0.021648854, -0.34295765, 0.15071863, -0.06619896, -0.084465064, 0.31909832, 0.015414661, 0.14930317, -0.11295768) * go_0(1.0, 0.0);\n    result += mat4(0.24530606, 0.25526014, 0.09971985, -0.07749641, -0.2361951, -0.07997673, 0.03617294, 0.02959561, -0.4498983, -0.014073485, -0.20587012, 0.06396779, 0.1262825, 0.027433183, 0.14469334, 0.011538011) * go_0(1.0, 1.0);\n    result += mat4(-0.038572453, -0.023108613, -0.039481267, -0.012160024, -0.004521989, -0.028665857, 0.04295255, 0.10580258, 0.05439479, -0.072261885, 0.11030243, 0.08934696, 0.09133867, 0.017547369, 0.097613186, 0.05491059) * go_1(-1.0, -1.0);\n    result += mat4(-0.09972817, 0.057730395, 0.12665828, 0.32861367, -0.16186063, 0.0745509, 0.2394045, -0.08687853, -0.034404907, -0.05843572, 0.0684561, -0.1355754, 0.19248672, -0.60372186, 0.12583947, 0.4388962) * go_1(-1.0, 0.0);\n    result += mat4(0.10341107, 0.061113223, 0.08773817, -0.082504354, -0.16612078, 0.2681751, 0.019737698, -0.17122322, -0.135949, 0.3048101, 0.087803006, 0.11373851, 0.013192192, -0.27022064, 0.35529897, -0.15321451) * go_1(-1.0, 1.0);\n    result += mat4(-0.032835662, 0.11123062, -0.11322452, -0.17300649, 0.04680824, 0.12849288, 0.17269878, -0.048671383, 0.05189037, -0.009078046, 0.22105052, 0.013008137, -0.009738674, 0.15391739, 0.20969556, 0.14189166) * go_1(0.0, -1.0);\n    result += mat4(-0.47377753, 0.3038031, 0.18604809, 0.1931698, -0.2964668, -0.12287907, -0.7107761, 0.26619422, -0.33923018, 0.19200724, 0.013786281, -0.17496964, 0.079325035, -0.3694445, 0.0054486147, -0.33018264) * go_1(0.0, 0.0);\n    result += mat4(0.14903802, -0.028043179, 1.5238678e-05, 0.021232028, 0.16025065, 0.14746875, -0.22831628, -0.12177345, 0.038778774, 0.32188168, -0.042017702, 0.27155936, 0.17920609, 0.04099755, 0.28527525, 0.074623376) * go_1(0.0, 1.0);\n    result += mat4(0.057019282, -0.112741895, 0.030361209, 0.14567861, 0.056265317, -0.01573537, -0.06707608, 0.016657263, 0.09829025, -0.026795063, 0.023042196, 0.09438241, -0.025483066, -0.052787006, 0.19730279, 0.021218104) * go_1(1.0, -1.0);\n    result += mat4(0.19868211, -0.01531125, 0.108596824, -0.035456363, 0.0033609823, 0.057961613, -0.013726211, 0.101742364, 0.33357215, 0.14468077, 0.29711527, -0.24662566, -0.119014986, -0.1899639, 0.11246697, -0.0035374009) * go_1(1.0, 0.0);\n    result += mat4(-0.05602109, -0.15539522, 0.010730943, 0.057116497, -0.02037749, 0.084210664, -0.028235348, 0.10574697, 0.056925274, 0.07922333, -0.090088, 0.1615985, -0.0044301567, -0.089945644, 0.024176618, -0.041844133) * go_1(1.0, 1.0);\n    result += vec4(0.0015292584, -0.043625206, -0.09429898, -0.06280405);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!SAVE conv2d_5_tf\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.06051604, -0.028152643, -0.21418124, 0.13032125, 0.42565975, -0.09571944, -0.34494513, 0.30004, -0.073245734, -0.028659137, 0.0032105136, -0.05009555, -0.048971225, 0.04814533, 0.002843805, -0.046224426) * go_0(-1.0, -1.0);\n    result += mat4(-0.07495975, 0.018714864, 0.21229684, -0.13614887, 0.79988647, -0.0697328, 0.38232988, 0.24165109, 0.25947478, -0.0009418982, -0.17369923, 0.10007766, 0.024117598, 0.028611807, 0.15090801, -0.06344829) * go_0(-1.0, 0.0);\n    result += mat4(-0.07982219, 0.0900347, 0.007609254, -0.0034791247, 0.013611781, -0.13560618, 0.09685799, 0.06276075, 0.134693, -0.14370437, -0.25175703, -0.0016138123, -0.0075672898, -0.13325731, -0.061100446, 0.0059743375) * go_0(-1.0, 1.0);\n    result += mat4(-0.039018434, -0.19668463, -0.43018532, 0.31886247, 0.4965479, 0.114569925, 0.19110382, 0.27343535, 0.0707728, -0.11877004, -0.25827697, 0.37012872, 0.1474777, 0.07056952, -0.14965728, 0.061595406) * go_0(0.0, -1.0);\n    result += mat4(0.506543, -0.16268773, 0.455319, -0.0702646, 0.70102173, -0.14041683, 0.70184857, 0.4817842, -0.3389246, -0.14463086, 0.13763213, -1.1259074, 0.47722015, 0.38352612, -0.04293366, -0.5604627) * go_0(0.0, 0.0);\n    result += mat4(0.17606944, 0.15897374, 0.13499324, 0.29241478, -0.032824475, 0.11128662, -0.22204424, -0.051803727, 0.013195331, -0.42040786, -0.3950585, 0.70745844, 0.38646924, -0.19080774, -0.15171832, -0.10742828) * go_0(0.0, 1.0);\n    result += mat4(-0.039278325, 0.18421806, -0.044948544, 0.07902063, -0.2149251, 0.09913459, -0.09743655, -0.26899317, -0.002695496, -0.07554527, -0.22373366, 0.17830558, -0.047994815, -0.06789183, -0.06755918, -0.104452066) * go_0(1.0, -1.0);\n    result += mat4(-0.0493473, -0.30411786, -0.056439694, -0.06582185, -0.21309847, 0.100670904, -0.22966193, -0.045954112, 0.12728062, -0.25081897, -0.094699375, -0.4036555, 0.060854495, -0.64373237, -0.21522263, -0.6683476) * go_0(1.0, 0.0);\n    result += mat4(0.063481025, 0.11744312, -0.043330096, 0.33817932, -0.06679828, -0.23207302, -0.10188898, -0.10590511, 0.058780864, 0.047292337, -0.11834696, 0.10076128, -0.036641665, 0.30200714, -0.0002892557, -0.10303763) * go_0(1.0, 1.0);\n    result += mat4(-0.10842604, 0.042055763, 0.29702973, -0.07409644, -0.030164458, -0.012098744, -0.06396587, -0.08787527, 0.051854923, 0.12997511, 0.11468497, 0.15022379, 0.007814715, 0.014517445, 0.025484756, 0.01078619) * go_1(-1.0, -1.0);\n    result += mat4(-0.29229385, 0.040265664, -0.15376821, 0.075579196, -0.05593569, -0.045405343, 0.12099204, 0.1571252, 0.17841713, 0.04673325, 0.14550509, 0.08603346, -0.049786013, 0.06121843, -0.16273825, -0.13857752) * go_1(-1.0, 0.0);\n    result += mat4(0.06903744, 0.2628764, -0.13582836, -0.35678583, -0.13821034, -0.019381443, -0.19570538, -0.09298511, 0.08965436, 0.09745909, 0.20055099, 0.024967568, 0.08144204, 0.004633625, 0.12809834, -0.009431525) * go_1(-1.0, 1.0);\n    result += mat4(0.09784006, 0.010729353, 0.046643205, -0.110926524, -0.21556224, 0.00016300633, 0.122175336, 0.15004392, 0.013864355, 0.24767809, 0.13865592, 0.0155424485, -0.1450483, -0.15688781, -0.06195043, -0.13745981) * go_1(0.0, -1.0);\n    result += mat4(0.018991318, 0.55401963, 0.11709872, -0.028442185, -0.46035343, -0.10215539, -0.60193926, 0.47882316, -0.23346989, 0.037200127, 0.22814943, -0.08231696, -0.36430013, -0.011152757, 0.48752213, 0.29796222) * go_1(0.0, 0.0);\n    result += mat4(-0.07258066, -0.023222538, 0.23230423, -0.30317304, 0.03942911, -0.06899803, 0.23778579, 0.07418621, -0.17443737, 0.33387753, 0.007354842, -0.123447575, -0.1745315, 0.11071779, -0.11949625, -0.22832453) * go_1(0.0, 1.0);\n    result += mat4(-0.024909232, -0.0308135, 0.12170621, -0.13298757, 0.045828197, -0.1532345, -0.06633672, 0.23591088, 0.04964077, 0.14091493, 0.038343724, -0.029780807, 0.05762822, -0.048930667, -0.02434709, 0.07109019) * go_1(1.0, -1.0);\n    result += mat4(-0.16039175, 0.3004474, -0.17278233, 0.13677922, 0.18838613, 0.15054552, 0.32901475, -0.1288333, 0.26378244, -0.05119892, 0.34533516, 0.25180495, 0.19452183, 0.0843233, -0.08029368, 0.39877903) * go_1(1.0, 0.0);\n    result += mat4(-0.07097129, -0.26492423, -0.055032317, -0.093516104, -0.11795062, 0.04086253, -0.07989471, 0.059686553, 0.09378249, 0.45851848, 0.2510942, 0.19599153, 0.019765077, -0.02920918, -0.04125142, -0.13859107) * go_1(1.0, 1.0);\n    result += vec4(0.04400571, -0.04015565, 0.0140529545, 0.05474095);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!SAVE conv2d_6_tf\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.014236042, -0.0031431736, -0.1551387, 0.12515116, -0.28528872, 0.36161992, 0.15750743, -0.17111474, 0.13792591, -0.0657419, -0.17471549, 0.14650472, 0.034169197, -0.019157575, 0.23520657, -0.20358163) * go_0(-1.0, -1.0);\n    result += mat4(0.02015035, 0.12993371, 0.11199667, -0.09854378, 0.5001741, 0.03462961, 0.24919736, 0.08505297, -0.20902094, -0.24141377, -0.15360375, 0.049974803, -0.037157424, -0.048510186, 0.20106035, -0.118480384) * go_0(-1.0, 0.0);\n    result += mat4(0.086798504, -0.009607818, 0.034812123, -0.005187592, 0.0351509, 0.021755, -0.04996161, -0.041231696, 0.0020545553, 0.015730752, -0.07507172, 0.018597523, -0.02393343, 0.07624775, 0.03892451, -0.0025574185) * go_0(-1.0, 1.0);\n    result += mat4(0.035725456, 0.06809103, 0.51926994, -0.39983147, -0.16402833, -0.1243394, -0.25922915, 0.28285915, 0.15959994, -0.2351732, 0.2650535, -0.30193794, -0.11468332, 0.050777763, -0.51894253, 0.4408367) * go_0(0.0, -1.0);\n    result += mat4(-0.27042082, 0.22243942, 0.14902467, 0.38428563, 0.46612173, 0.5169912, -0.22330502, -0.11300288, -0.36141354, 0.0668681, 0.2984152, 0.1275798, -0.24121419, 0.2952039, -0.45109174, -0.3822957) * go_0(0.0, 0.0);\n    result += mat4(0.26543504, -0.05742226, -0.052103903, -0.013124308, -0.14358385, -0.04024543, 0.07665455, -0.012301872, -0.18752757, -0.03913891, 0.038205814, -0.006583095, -0.25550908, -0.25725332, -0.12454206, -0.0058936924) * go_0(0.0, 1.0);\n    result += mat4(-0.0018946569, 0.019746022, -0.13080788, 0.11450627, -0.013743845, -0.027179785, -0.14425103, 0.07109661, 0.023703793, 0.086905524, 0.03151253, 0.0132474145, 0.041018624, 0.04548913, 0.2718715, -0.20008296) * go_0(1.0, -1.0);\n    result += mat4(-0.076830454, 0.11652955, 0.5068201, -0.3082819, 0.058615055, -0.006765798, -0.057522714, 0.049981344, -0.006897243, -0.21763432, 0.16896053, -0.21176189, -0.061227098, 0.03566485, 0.08901554, -0.050980624) * go_0(1.0, 0.0);\n    result += mat4(0.02327798, 0.07662976, 0.034811985, -0.03238033, -0.0021881019, -0.030997375, -0.069672935, 0.04040273, -0.1217442, 0.104173124, 0.09862539, 0.020557549, -0.022286594, 0.10287763, -0.021694934, 0.07542515) * go_0(1.0, 1.0);\n    result += mat4(0.124069154, -0.08579466, -0.07816314, 0.11332851, -0.034682628, -0.11038275, 0.04750615, -0.096100725, 0.039588403, -0.15149672, -0.05529172, 0.034304325, -0.022520235, -0.05023852, -0.2674731, 0.21886522) * go_1(-1.0, -1.0);\n    result += mat4(-0.1948599, -0.14946899, -0.39548838, 0.18042913, -0.007919619, 0.19826505, 0.23789087, 0.009140256, 0.11857748, 0.18215668, 0.13606293, -0.09209675, -0.080678545, -0.020431137, -0.07728839, -0.051353537) * go_1(-1.0, 0.0);\n    result += mat4(-0.07616472, -0.0032800382, -0.045657665, -0.039144326, -0.37786487, -0.08877774, 0.053579114, -0.070886396, 0.011311804, 0.107276045, 0.013236154, 0.009832061, 0.08292063, 0.12258811, 0.0005569043, -0.009806432) * go_1(-1.0, 1.0);\n    result += mat4(-0.28062925, 0.15946878, -0.1021801, -0.06471589, -0.26999477, 0.21230288, -0.14243907, 0.2555922, -0.09608517, 0.26339412, 0.20891234, -0.23538485, 0.33958244, -0.12569186, 0.43289876, -0.33462036) * go_1(0.0, -1.0);\n    result += mat4(0.16265294, 0.2625464, -0.34452894, 0.2233622, 0.13850005, -0.42999864, -0.5385177, -0.11035979, 0.51662, -0.78238726, -0.09422375, 0.83759475, 0.44468537, 0.14301361, 0.108906105, 1.1596143) * go_1(0.0, 0.0);\n    result += mat4(-0.73757625, -0.12369605, 0.23523071, 0.006587637, -0.15445381, 0.22757277, 0.052819528, 0.10183905, -0.07912228, -0.16998893, -0.13360223, 0.014348178, -0.17778571, -0.41047302, 0.10241381, -0.08526306) * go_1(0.0, 1.0);\n    result += mat4(0.14712952, 0.048995696, 0.05299946, -0.06817572, 0.1498064, -0.079825334, 0.40354064, -0.31789717, -0.1998377, 0.00955295, -0.32318407, 0.30898204, -0.039571725, -0.026203401, -0.16292085, 0.08574385) * go_1(1.0, -1.0);\n    result += mat4(-0.6353329, -0.56000775, -0.17279743, 0.18198174, -0.19555812, 0.056538377, 0.34365895, -0.07799055, 0.19011354, -0.13952748, 0.029196098, -0.19596763, -0.069196045, -0.17402656, 0.07948411, -0.016226962) * go_1(1.0, 0.0);\n    result += mat4(0.25592864, 0.083498634, -0.28515807, 0.10789751, 0.0043962947, 0.07085363, 0.048724182, -0.025131436, -0.0049440865, -0.033094388, -0.032935806, 0.04266025, 0.20026933, 0.0927841, -0.006839351, -0.013012285) * go_1(1.0, 1.0);\n    result += vec4(0.02021373, 0.0014037411, 0.0012718709, 0.017278494);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x1x1x56\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_1_tf\n//!BIND conv2d_2_tf\n//!BIND conv2d_3_tf\n//!BIND conv2d_4_tf\n//!BIND conv2d_5_tf\n//!BIND conv2d_6_tf\n//!SAVE conv2d_last_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.0067711817, 0.08160003, 0.0247279, 0.03084815, -0.026977416, -0.02120602, -0.025078611, -0.029852165, -0.011627478, -0.012742972, 0.022736797, -0.0028815821, -0.007515677, 0.0172887, -0.023259213, 0.009608947) * g_0;\n    result += mat4(-0.028660107, -0.014015208, -0.027838672, -0.013171922, 0.0029435428, 0.027047642, -0.017478354, 0.022834882, -0.037572853, -0.0034044068, -0.0149029335, -0.013362301, 0.009827443, -0.015742151, -0.0074795415, -0.0022266617) * g_1;\n    result += mat4(-0.07579662, -0.039754186, -0.066026606, -0.046816852, 0.1099032, 0.043956704, 0.073109835, 0.04680284, -0.06896613, -0.008838632, -0.044584926, -0.01319039, -0.0021152915, -0.04503326, 0.027061926, -0.028334105) * g_2;\n    result += mat4(0.15458213, 0.059769996, 0.09327123, -0.028782733, 0.023459995, -0.15390377, -0.13432898, -0.1127775, 0.072764635, -0.0020463336, 0.034736466, -0.0012086042, -0.05847183, -0.029952323, 0.052969377, 0.09590908) * g_3;\n    result += mat4(-0.07476772, -0.016574614, 0.04131183, 0.017335678, 0.009654406, 0.072183535, -0.002266456, 0.086873695, 9.310129e-05, 0.0056416965, -0.004188391, 0.023132093, -0.05183336, -0.025825873, -0.03684392, -0.0075729224) * g_4;\n    result += mat4(0.00878842, 0.03869637, -0.035759524, 0.003345386, -0.064184256, -0.034568302, -0.06672922, -0.0686381, -0.06794392, -0.10685906, 0.04679947, -0.012535639, 0.006932529, -0.007783515, 0.109123886, 0.13804391) * g_5;\n    result += mat4(-0.03160699, 0.050473, -0.09030729, 0.0649397, 0.11466501, 0.17912874, -0.0081851315, 0.052244574, 0.051632743, 0.061941486, 0.06546816, 0.12174249, -0.05104755, -0.018193979, -0.032196652, -0.035292786) * g_6;\n    result += mat4(0.013612735, -0.0024100312, -0.068611205, -0.07369285, -0.019647537, -0.066944756, -0.010012875, -0.06785739, -0.062246565, -0.087313406, -0.044278186, -0.09368995, 0.052555013, 0.13604961, 0.05645059, 0.08763303) * g_7;\n    result += mat4(0.04218486, -0.05028401, 0.059086576, -0.03545452, 0.027737848, 0.0043074046, 0.0011001764, -0.073026665, -0.04094988, 0.044061556, -0.009812515, 0.06841999, -0.06612581, 0.037223976, -0.07759491, -0.04356598) * g_8;\n    result += mat4(-0.027558247, 0.014248466, -0.019813016, -0.058107473, -0.016717663, -0.020424338, 0.0053625097, -0.009917319, 0.013678771, 0.0113340765, 0.0061787106, -0.036083996, -0.020179711, -0.011310535, 0.054827053, -0.0008278952) * g_9;\n    result += mat4(0.028690035, -0.012079616, 0.11931408, -0.048533775, 0.069336995, 0.0049852817, 0.013774468, 0.035233382, -0.07384821, 0.0003354423, -0.0059171803, -0.04503906, 0.08727279, 0.005138857, -0.17724465, 0.055782065) * g_10;\n    result += mat4(-0.20744391, 0.24348328, -0.3145766, 0.17026486, -0.022870807, -0.01648648, -0.05912279, -0.012555373, -0.066004686, 0.03182394, 0.16285324, -0.1221846, -0.31816196, 0.007928748, 0.43180224, -0.015949022) * g_11;\n    result += mat4(0.16363169, 0.14781676, -0.2377973, -0.1571377, -0.09038187, 0.0046504294, 0.033955004, -0.051421452, 0.046735536, 0.006827522, -0.121338, 0.12671822, 0.15833299, -0.1858712, -0.1942371, 0.17336044) * g_12;\n    result += mat4(-0.018145572, -0.015550516, 0.044410378, 0.046016492, 0.084021375, 0.05327457, -0.008270992, -0.045435544, 0.07185879, -0.131923, 0.26721445, -0.26745328, -0.07093472, 0.042701527, 0.13793674, -0.095621444) * g_13;\n    result += vec4(0.016836504, 0.010161949, 0.021351453, 0.01278978);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Depth-to-Space\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_last_tf\n//!SAVE MAIN\n//!WIDTH conv2d_last_tf.w 2 *\n//!HEIGHT conv2d_last_tf.h 2 *\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\nvec4 hook() {\n    vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size);\n    ivec2 i0 = ivec2(f0 * vec2(2.0));\n    float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x];\n    float c1 = c0;\n    float c2 = c1;\n    float c3 = c2;\n    return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Upscale_CNN_x2_S.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.0057322932, 0.12928207, -0.056848746, 0.18680117, -0.0306273, 0.25602463, 0.053723164, 0.20419341, 0.0018709862, 0.022848232, -0.04105527, 0.10169034, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(0.009471417, -0.12957802, 0.096014425, 0.21836184, 0.00021601951, -0.22997683, 0.23666254, 0.41192335, 0.021762101, 0.0047863554, 0.008233427, 0.108514786, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(-0.01156376, -0.18988979, 0.04614705, -0.044767227, 0.01050636, -0.26426336, 0.23741047, 0.0027636609, -0.027718676, -0.14202335, -0.016650287, -0.06637125, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(0.057809234, -0.11033858, 0.056533534, -0.06292466, 0.13880666, -0.18710336, 0.2441031, -0.25326246, 0.0032683122, -0.026437074, 0.0023248852, 7.640766e-05, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.49110603, 0.4429004, -0.44015464, -0.41174838, -0.87738293, 0.7808468, -1.0929365, -0.59699076, -0.18409836, 0.185138, -0.11773224, -0.17097276, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(0.10580959, -0.055947904, -0.03431237, -0.080236495, 0.14862584, -0.15393938, -0.18872876, -0.3170681, 0.03559387, -0.003990826, 0.021298569, 0.012844483, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(-0.040715586, -0.25781113, 0.08896714, -0.1225879, -0.15790503, -0.54010904, 0.29588607, 0.10401059, 0.003413123, -0.108357325, 0.0112870345, -0.11888622, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.0049315444, 0.02376202, -0.08224771, 0.121118225, -0.041512914, -0.027994309, -0.585988, -0.069672115, -0.017247835, 0.0056576864, 0.04319012, 0.055003505, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(0.37521392, 0.15916082, 0.059708964, 0.19046007, 0.8120325, 0.38343868, 0.3436578, 0.5287958, 0.16570656, 0.06957687, 0.014022592, 0.074799836, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(-0.01050964, -0.00939481, 0.17684458, 0.027366742);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.011029496, 0.05866063, -0.09460646, -0.017664742, -0.022488879, 0.18384217, -0.00397663, -0.064733066, 0.08466802, 0.10667488, 8.0212536e-05, 0.0908869, 0.13580276, 0.00097438256, 0.12176522, -0.08218466) * go_0(-1.0, -1.0);\n    result += mat4(0.16062798, -0.10190268, 0.03280682, 0.05621916, -0.009684231, -0.08464307, 0.17058301, -0.096469186, 0.1967505, -0.1450099, 0.093607284, -0.28240147, -0.21377413, 0.10079291, -0.1741522, 0.17330575) * go_0(-1.0, 0.0);\n    result += mat4(-0.060160473, 0.06316997, 0.0046929033, -0.049405966, 0.13851729, 0.06830702, -0.0586872, -0.040827133, 0.007052838, -0.03576886, -0.111261636, 0.039155316, -0.07380389, -0.09369825, 0.04471156, 0.09678487) * go_0(-1.0, 1.0);\n    result += mat4(-0.36683616, -0.035950605, -0.24414362, -0.009159744, 0.19335322, -0.099253505, 0.075083904, -0.00076695543, 0.65291303, -0.25599423, 0.19827642, 0.065899536, -0.07423247, -0.068967685, 0.0050554527, -0.060272824) * go_0(0.0, -1.0);\n    result += mat4(-0.020688485, -0.83178276, 0.11104878, 0.26454413, 0.13655476, 0.37675047, -0.22219229, -0.01751935, 0.44552696, 0.92510307, 0.16063261, -0.62011045, 0.19366647, -0.06996067, -0.2504841, 0.00803723) * go_0(0.0, 0.0);\n    result += mat4(0.0051537007, -0.057168536, -0.16110587, 0.25232598, -0.04447099, 0.11997351, 0.14808103, -0.34443566, -0.26212573, -0.21970181, 0.2724405, 0.21050811, -0.07949061, -0.064808235, -0.21208277, -0.0042361654) * go_0(0.0, 1.0);\n    result += mat4(-0.0888952, -0.20169449, 0.19144905, -0.016882861, -0.013283103, 0.07552998, -0.24686803, 0.012453213, -0.065454446, -0.016123284, -0.47316182, 0.070926026, 0.09219782, 0.13118166, 0.074736096, 0.0077910526) * go_0(1.0, -1.0);\n    result += mat4(0.5832154, 0.1138069, -0.039765622, 0.3182784, -0.25497997, 0.0013993139, 0.39285088, -0.48511526, -0.39891505, -0.19094779, -0.082146175, -0.20826934, 0.020590555, -0.0012490178, -0.4398621, 0.14377014) * go_0(1.0, 0.0);\n    result += mat4(0.21917395, 3.4314657e-05, 0.25734863, -0.3433305, 0.015720673, 0.2676127, -0.06807297, 0.15040149, -0.23638041, -0.0050233034, -0.13666134, 0.4542111, -0.033572577, -0.08450588, -0.23341487, 0.053490847) * go_0(1.0, 1.0);\n    result += mat4(-0.17482175, 0.057647135, 0.33135444, 0.0850751, -0.1718849, -0.0854123, 0.036795795, -0.13874969, -0.10903869, -0.19007301, -0.06064334, -0.03786032, -0.036696054, 0.07844446, 0.012523185, -0.01562906) * go_1(-1.0, -1.0);\n    result += mat4(-0.04411997, -0.10331819, 0.10050193, 0.12406485, 0.07431592, 0.30109692, -0.17511666, -0.13263564, -0.10192587, 0.07821255, -0.22415096, 0.25552443, 0.17881326, -0.13914281, 0.109979235, -0.0016463579) * go_1(-1.0, 0.0);\n    result += mat4(-0.01911644, -0.15412527, 0.028903123, 0.20831817, 0.00375175, 0.08110953, 0.074919395, -0.17581624, -0.015677985, 0.06504228, 0.08817818, -0.12518327, -0.09537373, 0.028905088, -0.051288474, 0.054334078) * go_1(-1.0, 1.0);\n    result += mat4(0.2852779, -0.28924024, 0.36805123, 0.21079305, -0.28336474, 0.1679663, -0.08641141, -0.10699407, -0.16090055, 0.1287612, -0.15910125, 0.05734755, 0.15883245, 0.0053026294, 0.080674745, 0.0505137) * go_1(0.0, -1.0);\n    result += mat4(0.17639062, 0.3790122, -0.19588692, -0.020314282, 0.26197383, 0.09014768, 0.19696823, -0.41025418, -0.08308115, -0.33279485, -0.22528782, 0.06172439, -0.1365661, -0.13094363, -0.005086559, 0.089024484) * go_1(0.0, 0.0);\n    result += mat4(0.05262993, 0.0006296959, 0.1657725, -0.32591924, 0.12126701, 0.061543245, -0.10526848, 0.041583937, 0.094976954, 0.09416157, -0.22019257, -0.058390073, -0.2073888, 0.057273377, 0.19558284, 0.004208022) * go_1(0.0, 1.0);\n    result += mat4(0.30005738, 0.18478931, -0.23342943, 0.22455733, -0.016488122, 0.099634305, 0.31620836, -0.15731157, 0.09595808, 0.0013774688, 0.48273298, -0.07027936, -0.18764344, -0.26194447, -0.11794225, -0.012173601) * go_1(1.0, -1.0);\n    result += mat4(0.117986746, -0.13846518, -0.019614812, -0.3011192, 0.5501164, 0.3408611, -0.40090847, 0.15706886, 0.13050972, 0.051776595, 0.20792943, 0.23389706, -0.22965533, -0.053367328, 0.3911586, -0.032988597) * go_1(1.0, 0.0);\n    result += mat4(0.054753624, -0.008485731, -0.2451672, 0.17528129, 0.13657846, 0.010480436, 0.07651423, -0.43316832, 0.12736236, 0.13804524, 0.12529011, -0.30946237, -0.14423579, 0.08403089, 0.24335162, 0.057288036) * go_1(1.0, 1.0);\n    result += vec4(0.012077211, 0.013045883, 0.0380778, -0.02908858);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.036115196, -0.06971895, -0.07508942, 0.016036168, 0.12120111, 0.24536026, 0.044755507, -0.20663576, 0.029635755, -0.15427187, 0.027148994, -0.20795093, 0.10170582, 0.077919215, 0.66063017, -0.4632968) * go_0(-1.0, -1.0);\n    result += mat4(-0.0052889925, -0.019060908, -0.08660142, -0.022095207, -0.08097976, -0.015142803, -0.18552722, -0.078493506, -0.16293915, -0.20099808, -0.08370822, 0.3701389, 0.09094984, 0.2487225, 0.24338846, 0.044003833) * go_0(-1.0, 0.0);\n    result += mat4(-0.061406493, -0.017232792, -0.10917424, 0.11203319, 0.040699825, -0.019294346, 0.084953666, -0.018133596, 0.07209552, 0.016069936, 0.17805555, -0.089537814, 0.15809004, 0.1027023, 0.15044671, -0.15530108) * go_0(-1.0, 1.0);\n    result += mat4(0.0948676, -0.040305693, -0.005591629, -0.048048403, -0.07547777, 0.056606572, 0.021390207, 0.32600567, -0.20805131, -0.099587254, 0.029613169, 0.0092129605, -0.29429698, -0.09898621, 0.44470885, -0.89487344) * go_0(0.0, -1.0);\n    result += mat4(-0.122259885, 0.11445877, 0.06666907, 0.1869428, -0.1553992, -0.1658741, 0.2988138, -0.57746625, -0.34609964, 0.11169158, -0.41877756, 0.38075635, 0.21293911, 0.09640372, -0.12754214, -0.08026104) * go_0(0.0, 0.0);\n    result += mat4(0.15128808, 0.050087795, 0.09219755, -0.18080945, 0.0044571217, -0.046019405, -0.1289922, 0.20305426, 0.19601224, 0.04667917, 0.17465587, 0.027672665, 0.18441725, 0.06845396, 0.11288585, -0.23283863) * go_0(0.0, 1.0);\n    result += mat4(-0.072962, -0.06639447, 0.049347494, -0.1386401, 0.10396071, 0.08187777, -0.04280746, 0.07390891, 0.06628344, 0.037797406, 0.021885803, -0.013147403, 0.22376558, 0.36243078, 0.12874891, -0.0023783944) * go_0(1.0, -1.0);\n    result += mat4(0.074945286, 0.16045591, -0.11798349, 0.12910712, 0.054760084, -0.095626175, -0.047832094, 0.03493912, 0.11817307, 0.037452437, -0.14301221, -0.027356789, -0.052390423, 0.11373512, 0.07686775, 0.010008694) * go_0(1.0, 0.0);\n    result += mat4(-0.023999173, -0.091900624, 0.02388157, 0.03173873, 0.0065633506, -0.033716757, -0.1198324, 0.12057766, 0.026465805, -0.07517131, -0.07760598, 0.060463097, 0.07345541, 0.046037503, 0.21101558, -0.26785463) * go_0(1.0, 1.0);\n    result += mat4(0.15544604, -0.03902825, 0.04630384, -0.25173616, -0.0691359, 0.07476507, 0.009071253, 0.089964196, -0.26539803, -0.3958477, -0.22155671, 0.20735882, -0.105860494, -0.003996804, -0.044815883, 0.39544627) * go_1(-1.0, -1.0);\n    result += mat4(0.6169709, 0.23717614, -0.37884676, -0.7484867, 0.020169826, -0.30718836, 1.0965588, -0.20711036, -0.39149985, -0.06843563, -0.06522909, 0.103805855, 0.03265825, -0.15137726, 0.12837899, -0.01294922) * go_1(-1.0, 0.0);\n    result += mat4(-0.23638196, -0.4560866, -0.11948684, -0.1464144, 0.10690008, 0.007835961, 0.11864342, -0.13101323, -0.16509797, 0.075027354, 0.08122998, 0.13451207, 0.0011890623, 0.052157886, 0.08372405, -0.07085038) * go_1(-1.0, 1.0);\n    result += mat4(-0.21997726, -0.16488647, -0.0291317, 0.17997476, 0.1493211, 0.027494298, 0.0034613227, -0.3207727, 0.18699001, 0.14728633, -0.042895135, -0.07612043, 0.125076, -0.14714554, -0.03480009, -0.22753975) * go_1(0.0, -1.0);\n    result += mat4(-0.5342686, -0.7426105, -0.38294584, 0.42549992, 0.46053204, 0.7867879, 0.106234804, -0.041163098, 0.5198579, -0.5219404, 0.14809476, -0.41802374, 0.06810794, -0.15122683, -0.047409, 0.13178343) * go_1(0.0, 0.0);\n    result += mat4(-0.50428164, 0.18220626, 0.35510704, -0.081787474, 0.03155813, 0.019284263, 0.0032388573, -0.20513348, -0.05385551, 0.17803182, -0.26206362, 0.2870375, 0.008557827, 0.08401449, -0.027598893, -0.010791235) * go_1(0.0, 1.0);\n    result += mat4(0.16657415, 0.067647465, 0.093076974, -0.14438486, -0.10017002, 0.0022367141, 0.03250936, -0.052794546, -0.009178676, -0.019673595, -0.0016697067, -0.15424626, -0.112123474, -0.11079971, 0.011987111, -0.11747758) * go_1(1.0, -1.0);\n    result += mat4(-0.023021797, -0.058703423, -0.037978355, -0.062433913, -0.13130441, 0.048656322, 0.056839373, 0.109036915, -0.07823158, 0.14785293, 0.058555078, -0.11679035, -0.14002073, 0.07395252, 0.098268874, -0.06710464) * go_1(1.0, 0.0);\n    result += mat4(0.14906375, 0.030001195, -0.10338215, 0.0662968, -0.161953, -0.13682815, 0.09563142, 0.009514228, -0.009491218, 0.06737101, -0.1393389, 0.15231515, -0.073147796, 0.00767062, 0.028675212, 0.014213088) * go_1(1.0, 1.0);\n    result += vec4(0.018736731, -0.0026039074, 0.050130025, -0.055364225);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!SAVE conv2d_last_tf\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.019100675, -0.014241565, 0.004667036, -0.03865062, 0.106731094, 0.026099661, 0.014594411, -0.011881356, 0.0040967264, -0.004626336, 0.006469508, 0.010875305, -0.033909045, -0.085905954, 0.07861378, 0.019452631) * go_0(-1.0, -1.0);\n    result += mat4(0.20777655, -0.060354974, 0.0023840065, -0.064121604, -0.17397617, 0.019293457, -0.09707183, 0.080641985, 0.01025124, -0.017382381, 0.008661793, -0.010995665, 0.21943407, -0.115574986, 0.14471593, -0.068836235) * go_0(-1.0, 0.0);\n    result += mat4(0.057942886, -0.06311754, 0.2253396, -0.04159292, -0.020731755, 0.007877151, 0.041525815, 0.025278691, 0.03041967, -0.025137542, 0.024364179, -0.024543528, 0.029438615, -0.015506873, 0.081686, -0.07812221) * go_0(-1.0, 1.0);\n    result += mat4(0.054237515, 0.0676094, -0.0047708177, 0.0043467237, -0.10032304, -0.020498628, 0.04240586, 0.07272254, 0.0784221, 0.017945962, -0.022310399, -0.013134622, 0.015638694, -0.10001543, 0.1043031, 0.05898838) * go_0(0.0, -1.0);\n    result += mat4(-0.021652509, 0.35796642, 0.059497777, 0.23948468, 0.15454951, -0.10017235, -0.19072174, -0.44812536, -0.03974552, 0.04529369, 0.22207436, 0.026222564, -0.09705454, 0.5623026, -0.3354105, -0.017278556) * go_0(0.0, 0.0);\n    result += mat4(-0.053682446, -0.03411237, -0.09399936, 0.15128824, -0.07463, -0.042020727, 0.0031783928, 0.13481957, -0.07731454, 0.044114403, -0.23085599, 0.060444202, -0.15015422, 0.0018040676, -0.18684982, 0.2812511) * go_0(0.0, 1.0);\n    result += mat4(0.0029329916, 0.001596018, 0.0007512241, 0.016544111, -0.04876942, -0.05272409, 0.037884697, 0.049948208, 0.015518177, 0.11368592, -0.03815777, -0.013149978, -0.027638039, 0.107719295, -0.04115787, 0.02745414) * go_0(1.0, -1.0);\n    result += mat4(0.016691081, 0.010204119, 0.04078854, 0.01613337, 0.03325829, 0.0114824055, -0.017286912, -0.07284126, -0.110984206, -0.21041764, 0.0089543555, 0.18986733, 0.01537506, -0.2059135, 0.029074017, 0.013117443) * go_0(1.0, 0.0);\n    result += mat4(0.013965926, 0.029871881, 0.0034499036, -0.011343668, 0.022120327, -0.0068748263, 0.009324342, -0.039081004, 0.08032371, 0.050809264, 0.035050742, -0.2032847, 0.06305391, -0.021958945, 0.038569167, -0.22465245) * go_0(1.0, 1.0);\n    result += mat4(0.046307724, -0.012419472, 0.007673863, -0.042344846, 0.011042414, 0.016994251, -0.018166406, -0.016955731, -0.13240299, 0.01768431, -0.027607648, 0.0699927, -0.02840628, 0.004414203, 0.0049618417, 0.011084679) * go_1(-1.0, -1.0);\n    result += mat4(-0.119954154, -0.007455482, -0.031108133, -0.009946449, 0.0077065965, 0.01660345, 0.032943666, 0.016376585, 0.10273124, 0.1556573, -0.24643841, 0.107307844, -0.068235755, 0.0561896, -0.0104672015, 0.042693343) * go_1(-1.0, 0.0);\n    result += mat4(-0.01634601, 0.04195375, -0.10401894, 0.047641944, -0.034602515, -0.0034419263, -0.010457858, 0.015194475, -0.03962551, -0.030031368, 0.16036317, 0.019283568, -0.05877721, 0.016504882, -0.15523468, 0.018161612) * go_1(-1.0, 1.0);\n    result += mat4(-0.08083991, 0.0024665035, -0.049373373, 0.030371357, 0.0113322195, -0.014676956, 0.011646689, -0.01142667, 0.124930486, 0.06625774, -0.045840867, -0.009693036, -0.012649251, -0.07388084, 0.008790075, 0.0013844534) * go_1(0.0, -1.0);\n    result += mat4(-0.33941835, -0.2763476, -0.118311435, -0.063535266, 0.20936015, 0.13731301, 0.13443594, 0.07464433, 0.059650812, -0.36973104, 0.16444235, -0.37082872, 0.06432777, -0.18283032, -0.044489607, -0.13895285) * go_1(0.0, 0.0);\n    result += mat4(0.13533665, 0.08268915, -0.03675727, -0.14348659, 0.0186255, -0.05051692, 0.056702953, 0.0061717895, 0.047663026, -0.088188455, 0.23254345, -0.014015464, 0.08400204, -0.0073777726, 0.2202068, -0.12366078) * go_1(0.0, 1.0);\n    result += mat4(0.04361004, 0.046543695, 0.0064863074, -0.03358146, -0.022602187, 0.018138997, -0.011071864, 0.010244091, -0.019814799, -0.17250171, 0.040823266, -0.040131986, 0.010125854, 0.020660749, 0.0020435036, -0.010819304) * go_1(1.0, -1.0);\n    result += mat4(-0.004810193, -0.11286074, 0.051985834, 0.04788631, -0.023950428, 0.036145125, -0.038203828, 0.052401308, 0.022986965, 0.26420745, -0.06076917, -0.09252999, 0.03164547, 0.15652153, -0.037934, -0.0035418556) * go_1(1.0, 0.0);\n    result += mat4(0.03358366, -0.005219482, 0.007060882, -0.06569114, -0.02941682, 0.00966056, -0.0153679885, 0.019905418, -0.107232265, -0.03405676, -0.044340115, 0.26892832, -0.04723829, -0.02589829, 0.004563232, 0.19318114) * go_1(1.0, 1.0);\n    result += vec4(-0.00346731, -0.0046263863, -0.004627155, -0.0057769152);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Depth-to-Space\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_last_tf\n//!SAVE MAIN\n//!WIDTH conv2d_last_tf.w 2 *\n//!HEIGHT conv2d_last_tf.h 2 *\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\nvec4 hook() {\n    vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size);\n    ivec2 i0 = ivec2(f0 * vec2(2.0));\n    float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x];\n    float c1 = c0;\n    float c2 = c1;\n    float c3 = c2;\n    return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/Anime4K_Upscale_CNN_x2_VL.glsl",
    "content": "// MIT License\n\n// Copyright (c) 2019-2021 bloc97\n// All rights reserved.\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(0.3053028, -0.037464816, 0.113983095, 0.12537485, -0.18630321, 0.084269725, -0.01351514, -0.20190673, -0.12298384, -0.037622184, -0.070214555, -0.19367279, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(-0.41849324, 0.099702746, -0.04276645, -0.047299717, 0.20074473, 0.14217933, 0.15571699, 0.19553481, 0.21868695, -0.053848714, 0.016413521, 0.14117444, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(0.030540446, -0.052293833, 0.0715466, -0.31160545, 0.07808315, -0.16860045, 0.032828577, -0.2955024, -0.110374965, 0.04043687, -0.014024628, 0.058699366, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(-0.10727635, 0.054200135, 0.20853694, 0.21086875, 0.122690216, -0.091823794, 0.310609, -0.01738923, -0.0013488946, 0.10835534, -0.077265196, 0.086751856, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.77150255, 0.40530515, -0.41257596, -0.14367618, 0.46888494, 0.2650122, -0.934199, 0.40476102, 0.32293493, 0.20251967, 0.19891106, -0.29698747, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(-0.12505147, -0.41904053, -0.065798186, 0.34075752, 0.026240354, -0.2977496, 0.032647505, -0.003566783, 0.10290523, -0.23417123, -0.06014203, 0.094735645, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(0.11207838, -0.04062474, 0.023897955, 0.08605987, -0.020888371, 0.045541205, -0.07231824, -0.25884083, -0.11796847, -0.002691391, 0.0050435597, 0.02756291, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.4615728, 0.041790638, 0.08971143, 0.20213957, -0.38537467, 0.19938901, 0.08594364, -0.08621994, -0.08163473, -0.133266, -0.09561729, -0.014209637, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(0.0787417, -0.0483673, 0.07621572, -0.060169693, -0.013465177, -0.17152289, 0.02515561, 0.17675288, -0.05173998, 0.10768042, -0.029858522, -0.013957215, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(0.0072128535, -0.05658625, 0.052939568, -0.1760861);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3\n//!HOOK MAIN\n//!BIND MAIN\n//!SAVE conv2d_tf1\n//!WIDTH MAIN.w\n//!HEIGHT MAIN.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off)))\nvec4 hook() {\n    vec4 result = mat4(-0.112743355, 0.0422517, 0.21350034, -0.0967133, 0.16265953, 0.0022497, 0.015078242, 0.08204187, 0.035236806, -0.0468228, -0.09464228, -0.001864949, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, -1.0);\n    result += mat4(0.25631642, -0.41485596, -0.16662048, 0.13201024, 0.057921384, 0.2240005, -0.30038536, -0.08305622, 0.2228756, 0.32263795, 0.10608189, -0.18616734, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 0.0);\n    result += mat4(0.08997524, 0.11516871, 0.19212262, -0.035154644, 0.11612274, -0.04056247, 0.14974374, 0.029173585, -0.07629641, -0.14353512, 0.041081246, 0.20230265, 0.0, 0.0, 0.0, 0.0) * go_0(-1.0, 1.0);\n    result += mat4(0.2262286, 0.055954933, -0.14499907, 0.17314723, 0.16590612, -0.06688698, -0.11118816, -0.012938116, -0.043101817, 0.026133137, 0.2958395, 0.06543993, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, -1.0);\n    result += mat4(-0.07311521, -0.3041244, -0.47978505, -0.6350967, -0.17432262, 0.34965977, 0.25399777, -0.16590433, -0.49957857, 0.0549526, -0.40869385, -0.08780993, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 0.0);\n    result += mat4(-0.3014447, -0.00021343959, -0.14953177, 0.028001398, -0.14931908, -0.14910097, -0.13287953, -0.45026535, 0.17378895, 0.024704922, -0.027308129, -0.10292025, 0.0, 0.0, 0.0, 0.0) * go_0(0.0, 1.0);\n    result += mat4(-0.06732655, -0.13119644, 0.066014715, 0.081011154, -0.15154321, 0.2407805, 0.07733481, 0.12312706, 0.1741804, 0.008495716, -0.14125362, -0.043644864, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, -1.0);\n    result += mat4(0.11465958, 0.42001364, 0.011069392, 0.3203028, -0.058801666, -0.37830314, -0.030540617, 0.2245139, -0.11310525, -0.14845212, 0.19957744, 0.25789997, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 0.0);\n    result += mat4(-0.16037206, 0.21326372, 0.020099448, 0.018666709, 0.122083254, -0.16033986, -0.10725163, 0.2556128, 0.1650688, -0.10475823, 0.048623525, -0.103755645, 0.0, 0.0, 0.0, 0.0) * go_0(1.0, 1.0);\n    result += vec4(0.007717166, -0.027800834, 0.0795002, 0.0053199283);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!SAVE conv2d_1_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.0056740534, -0.21186607, -0.18014967, 0.118979976, -0.0015611284, -0.07708486, 0.060131397, 0.11653345, 0.027150517, 0.10837246, 0.08583816, -0.14032431, 0.017552888, 0.0035846964, 0.03980114, 0.064649396) * go_0(-1.0, -1.0);\n    result += mat4(-0.03289318, -0.12004539, 0.26514888, -0.15079662, 0.04214227, -0.027273783, -0.027950313, 0.19614808, 0.18510003, -0.10346252, -0.029836183, 0.09174428, -0.0088710375, -0.18273513, 0.06601674, 0.009983851) * go_0(-1.0, 0.0);\n    result += mat4(0.08476211, 0.043996535, 0.056711517, 0.009976895, 0.07039107, -0.024862664, -0.059921104, 0.046850603, 0.04983447, 0.04863198, 0.21777405, -0.0576961, 0.045321796, -0.0060038245, 0.096396215, -0.10842004) * go_0(-1.0, 1.0);\n    result += mat4(-0.15746164, 0.041757874, 0.035169285, -0.1734288, -0.24219254, -0.13318908, 0.2272079, -0.02902605, 0.07750601, -0.1467191, -0.12296749, -0.07533314, -0.07073083, 0.17909113, 0.04789308, 0.17245363) * go_0(0.0, -1.0);\n    result += mat4(0.057547905, 0.1464685, -0.33115456, -0.26956198, -0.26298407, -0.059824817, 0.022509675, -0.09251868, 0.36277944, -0.2072429, 0.21095088, -0.45492023, 0.07428653, 0.1593302, -0.2945834, 0.12825087) * go_0(0.0, 0.0);\n    result += mat4(-0.1318458, 0.27804148, 0.037600737, 0.12047866, 0.0065036337, 0.0017241207, 0.060497303, -0.14786585, -0.15149063, 0.02731698, 0.048886403, -0.0025970868, -0.026979815, 0.07348884, 0.015636757, -0.107966796) * go_0(0.0, 1.0);\n    result += mat4(-0.079988025, -0.01626299, 0.06517438, 0.086406484, -0.1484504, 0.070595, 0.20620634, 0.09713373, -0.13620836, 0.012067949, -0.00068703433, -0.038030174, 0.22300471, -0.0012400965, -0.014827909, -0.08927486) * go_0(1.0, -1.0);\n    result += mat4(0.15634936, 0.052028038, 0.038081627, 0.12720168, 0.07342066, -0.04318368, -0.0065998454, 0.12109317, -0.45398173, 0.03666754, -0.17773737, 0.038516667, -0.13009632, -0.007457001, -0.013938809, 0.09776142) * go_0(1.0, 0.0);\n    result += mat4(0.029636936, 0.12864171, 0.11347291, -0.11812842, -0.0870342, 0.035678383, 0.050338242, 0.045754932, -0.07072752, 0.010447726, 0.039642975, -0.08795004, -0.1191525, 0.00967509, 0.13485421, -0.053204738) * go_0(1.0, 1.0);\n    result += mat4(-0.011072695, -0.09613245, -0.09094804, 0.028029291, -0.04031162, 0.15690295, 0.25094184, -0.21776834, 0.06524669, 0.06412185, -0.052852992, -0.08097702, -0.039127756, 0.036357917, 0.104585476, 0.25095442) * go_1(-1.0, -1.0);\n    result += mat4(-0.08328618, -0.006246033, 0.099708706, -0.014916097, 0.17727195, 0.4369228, 0.14760216, 0.06707674, 0.025167737, -0.022487842, -0.038962565, 0.15380669, 0.08125089, 0.09844594, 0.33538374, -0.003161368) * go_1(-1.0, 0.0);\n    result += mat4(-0.0128195705, -0.05475118, -0.037705053, -0.0012077648, -0.17425515, 0.091487505, -0.12909423, 0.0074876705, 0.13438368, 5.778033e-05, 0.04563314, -0.12185897, -0.053612474, -0.049824294, -0.12851205, 0.12856449) * go_1(-1.0, 1.0);\n    result += mat4(-0.025741795, 0.01867236, -0.00027440622, 0.10502768, 0.27042285, -0.14947751, 0.11143123, 0.2575913, -0.07414089, -0.33919522, -0.13194235, -0.20088726, 0.23121537, -0.08197353, 0.06693911, 0.015411386) * go_1(0.0, -1.0);\n    result += mat4(0.09143717, 0.22842278, 0.06501074, -0.20009698, -0.042117566, -0.23452093, -0.074082755, -0.10612558, 0.077631965, 0.08343657, -0.07657599, -0.43297377, 0.7092466, -0.16272525, 0.17222248, -0.056038965) * go_1(0.0, 0.0);\n    result += mat4(0.081200436, 0.046752565, 0.028254949, 0.18820632, 0.096592255, 0.05896745, 0.14845169, 0.034777895, 0.07195204, -0.1908046, -0.015341971, 0.02606145, -0.010377239, 0.0755547, -0.15285216, 0.047916733) * go_1(0.0, 1.0);\n    result += mat4(-0.06825636, -0.049540907, -0.024328846, 0.03506251, 0.2060094, 0.054119263, -0.06671269, 0.052428722, 0.055792283, -0.14336903, -0.03180757, 0.013760968, -0.037398104, -0.06880077, -0.023608573, 0.0360965) * go_1(1.0, -1.0);\n    result += mat4(-0.16937497, -0.30156836, 0.0021435453, 0.025772978, -0.17990975, 0.046133514, -0.32447076, -0.083382785, -0.081322014, -0.022132374, -0.05319431, 0.11794733, 0.08943906, 0.12927428, 0.105764806, -0.051034793) * go_1(1.0, 0.0);\n    result += mat4(-0.011012306, 0.047636557, 0.050260928, 0.051847618, 0.010985655, -0.13752967, 0.023869954, 0.07011459, -0.18244945, 0.07239806, -0.013638856, -0.026982805, 0.11395993, -0.031304818, -0.08714153, 0.077115685) * go_1(1.0, 1.0);\n    result += mat4(0.08707592, 0.2265186, 0.13363098, -0.039588258, -0.029561255, 0.019238092, 0.024606103, -0.0019022018, -0.062285982, -0.0629511, -0.03753033, 0.109805316, 0.016018672, -0.08284564, -0.04092752, -0.030386891) * go_2(-1.0, -1.0);\n    result += mat4(0.0016500859, 0.01616536, -0.099148355, 0.24161765, 0.028064307, -0.028680569, 0.054400917, -0.1978921, -0.08584302, -0.096797146, -0.06546965, -0.09342837, 0.030265866, 0.07057579, -0.02080932, 0.053178705) * go_2(-1.0, 0.0);\n    result += mat4(-0.030304352, 0.047440585, -0.04248429, 0.08568772, -0.051317703, 0.036739342, 0.00865767, -0.018183297, -0.07335176, 0.025001721, -0.068509035, 0.1814819, -0.09756565, -0.024179723, -0.05959287, 0.0352454) * go_2(-1.0, 1.0);\n    result += mat4(0.023015196, -0.022870664, -0.12028372, -0.111095205, 0.11065281, -0.19900022, -0.24012049, -0.017028643, -0.13484617, 0.050107025, 0.10741765, 0.037951697, 0.013090438, -0.0010045726, -0.029447839, -0.1859787) * go_2(0.0, -1.0);\n    result += mat4(0.17922719, -0.24138594, -0.44595388, -0.032014426, 0.06897096, 0.07125395, 0.1944457, -0.035794795, -0.24022278, -0.13230884, -0.1277025, 0.21229011, -0.12249393, 0.06141907, 0.2687936, -0.26896995) * go_2(0.0, 0.0);\n    result += mat4(0.0397242, -0.30710965, 0.28815824, -0.06642567, -0.07588877, -0.019552408, 0.0057806037, 0.11465521, 0.03560534, -0.10640553, 0.023589289, -0.16667193, 0.02066607, -0.01026633, -0.02655378, 0.082493655) * go_2(0.0, 1.0);\n    result += mat4(-0.007902949, -0.08501038, -0.029395591, -0.07072227, -0.01800967, -0.14564751, -0.08372804, -0.049974415, 0.1756957, -0.02042449, -0.04413007, -0.016873527, -0.2385717, -0.001741017, 0.08298281, -0.019873247) * go_2(1.0, -1.0);\n    result += mat4(-0.01803727, 0.0642893, 0.21513617, 0.066888265, -0.042107955, -0.123470366, 0.045296013, -0.11958806, 0.48208967, -0.027188249, 0.12136116, 0.05246265, 0.13522038, -0.016297493, 0.028486907, -0.059840377) * go_2(1.0, 0.0);\n    result += mat4(-0.1373251, -0.11281026, -0.06418318, 0.08444032, 0.062874556, -0.009133875, -0.049571835, -0.042995855, 0.12483249, -0.025967957, -0.11202483, 0.09862257, 0.099986054, 0.009230306, -0.09042664, 0.046612263) * go_2(1.0, 1.0);\n    result += mat4(0.03203309, 0.106030256, 0.045741174, -0.020529225, -0.028610658, -0.055219248, -0.21404657, 0.07746393, -0.059359375, 0.0033258004, -0.0054513607, 0.06856653, 0.18043655, -0.119936846, -0.05639265, -0.10240379) * go_3(-1.0, -1.0);\n    result += mat4(-0.0004331875, 0.10426754, -0.008130048, 0.012795991, -0.14372933, -0.40797862, 0.105197415, -0.0041354536, -0.079792455, 0.0914027, 0.012418237, -0.11449173, 0.020261409, -0.14681602, -0.13355242, 0.18290488) * go_3(-1.0, 0.0);\n    result += mat4(0.052306626, 0.010864275, -0.072627716, -0.009773121, 0.09484167, -0.09631301, 0.14896165, -0.21220942, -0.11994051, -0.002957136, -0.118194886, 0.08661347, 0.10005298, -0.029620873, 0.101668894, 0.0242806) * go_3(-1.0, 1.0);\n    result += mat4(-0.055188183, -0.06322889, 0.12994595, 0.03140751, -0.092755616, 0.04239107, 0.18460171, 0.08471877, 0.014203371, 0.13608724, 0.035351243, -0.07883493, -0.10067456, 0.14417742, 0.0054235114, 0.100745104) * go_3(0.0, -1.0);\n    result += mat4(-0.043811034, -0.16055201, -0.11927185, 0.20517266, 0.16734722, 0.27720267, 0.1205665, 0.045803893, -0.07874647, 0.06764307, -0.11157022, 0.080770165, -0.044105835, -0.03276538, -0.10945451, 0.100562036) * go_3(0.0, 0.0);\n    result += mat4(-0.044731796, -0.12854387, -0.061937924, -0.21604767, -0.036132332, -0.024353411, -0.16718283, 0.14903957, -0.11620588, 0.14563644, 0.23363836, 0.08400659, 0.15248756, -0.1424437, 0.112882614, -0.04096889) * go_3(0.0, 1.0);\n    result += mat4(-0.0486021, -0.05714939, 0.042517707, -0.06106919, -0.12970918, -0.071898215, -0.044727243, -0.026308542, 0.05687118, -0.0394057, -0.109454155, -0.0021216893, 0.018588595, 0.08061093, 0.0500373, -0.0034918839) * go_3(1.0, -1.0);\n    result += mat4(0.11269324, -0.17924047, -0.12965205, -0.07287767, -0.015830642, -0.044497102, 0.20014328, -0.14054494, 0.1232692, 0.2395109, 0.14093149, 0.03518561, -0.14088139, -0.09045081, -0.07283352, 0.053434785) * go_3(1.0, 0.0);\n    result += mat4(0.020512339, 0.026349569, -0.06666101, 0.05554806, -0.03044066, 0.26656216, 0.019155584, -0.12118906, 0.087923005, -0.1716557, 0.050843164, 0.037432503, -0.030232614, 0.030457936, 0.04232163, -0.066400655) * go_3(1.0, 1.0);\n    result += vec4(-0.0216415, 0.09015036, -0.030761974, -0.26541537);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!SAVE conv2d_1_tf1\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.04688368, 0.13853125, 0.1714716, -0.03034447, -0.08090605, 0.1225867, 0.17535992, 0.012508419, -0.0010665918, -0.07481546, -0.15541986, 0.0671128, -0.029307734, -0.076674186, 0.03925896, -0.07140553) * go_0(-1.0, -1.0);\n    result += mat4(-0.13273083, 0.062933214, 0.04200143, -0.0080243945, -0.120439716, -0.090192355, -0.022639645, 0.00020024918, -0.11211478, -0.12949537, 0.025783822, 0.009155746, 0.01004339, -0.0661901, 0.10630156, 0.053137038) * go_0(-1.0, 0.0);\n    result += mat4(0.07113487, -0.16011865, -0.10838903, -0.0034704183, 0.110606894, -0.14915739, 0.036511585, -0.003103608, -0.0551775, -0.13140677, 0.05270299, 0.12139221, 0.02226174, 0.008415268, -0.06647426, 0.118130066) * go_0(-1.0, 1.0);\n    result += mat4(-0.045172617, -0.0020388453, -0.27287582, 0.002428232, -0.2833772, 0.13788106, 0.073339015, 0.10666715, 0.08455194, 0.16499293, 0.089058325, 0.008815447, 0.034657538, -0.109856166, -0.11499077, -0.02918854) * go_0(0.0, -1.0);\n    result += mat4(0.07910854, -0.26334837, -0.3246593, -0.08246522, 0.09211476, 0.40793833, -0.09658794, -0.14430091, -0.50632644, 0.087234974, 0.26298127, 0.3687086, 0.06492316, 0.23082961, 0.18233871, -0.09283792) * go_0(0.0, 0.0);\n    result += mat4(-0.022744032, 0.21690565, 0.2694824, -0.12230013, -0.07969618, 0.21595429, -0.034979805, 0.008938489, 0.21289209, -0.446482, -0.042927746, -0.13587558, -0.032581557, -0.07182814, -0.054092336, -0.009542036) * go_0(0.0, 1.0);\n    result += mat4(-0.0034912943, -0.080354184, -0.08577375, -0.1521193, 0.09809233, 0.034529503, -0.100664355, 0.008191219, -0.014303411, -0.02862216, -0.18669915, -0.12384598, 0.046499267, 0.093707144, 0.10661308, 0.15079576) * go_0(1.0, -1.0);\n    result += mat4(-0.031025652, -0.0384342, 0.14258307, 0.25531343, 0.0075049917, -0.03966595, 0.062381975, 0.19593526, -0.2868182, 0.03162008, -0.4391041, -0.524017, -0.034463473, -0.0066741486, -0.24586639, 0.10521736) * go_0(1.0, 0.0);\n    result += mat4(-0.07452321, -0.0227877, -0.025402244, 0.115727395, -0.039511252, -0.07785703, -0.013689458, 0.0066024344, -0.052957747, 0.011206241, -0.0021671024, 0.077190824, -0.11709912, 0.046635598, 0.123751156, -0.03712064) * go_0(1.0, 1.0);\n    result += mat4(0.055411004, -0.0020031065, 0.06685547, -0.018829947, -0.06378933, -0.18389674, -0.0023551763, 0.0670314, 0.13038594, 0.0601923, -0.03035789, -0.019537423, -0.014483204, -0.056800704, 0.08663347, -0.106859975) * go_1(-1.0, -1.0);\n    result += mat4(-0.06603686, 0.07360526, -0.0072026253, -0.06778907, -0.039178446, 0.012397263, -0.13482279, 0.05745685, -0.055182382, -0.10545766, 0.003857615, 0.041947857, -0.15239377, 0.041826613, 0.058879383, -0.0042669442) * go_1(-1.0, 0.0);\n    result += mat4(-0.0697229, -0.010702144, -0.032265816, 0.013317131, 0.105028264, 0.21032134, 0.06845646, -0.018358687, 0.064568676, 0.08437135, -0.000723181, 0.1324007, 0.05527932, -0.049871888, -0.10125047, -0.005040889) * go_1(-1.0, 1.0);\n    result += mat4(-0.006467578, -0.05120533, -0.011780779, -0.011742203, -0.34242442, -0.020819988, 0.17381702, -0.059836414, -0.028882682, 0.23210457, 0.16579404, -0.03708216, -0.23541835, -0.03290251, 0.029319672, 0.26189178) * go_1(0.0, -1.0);\n    result += mat4(-0.30955994, -0.06408282, -0.16872866, 0.10767772, -0.041430887, 0.051697977, 0.12523535, -0.060389146, 0.026289431, 0.06359533, 0.13526368, 0.2479901, -0.3263977, 0.10216362, -0.0030894123, 0.046437826) * go_1(0.0, 0.0);\n    result += mat4(0.10061438, -0.17047118, -0.21593021, -0.023389054, -0.17507865, -0.30822313, -0.22044766, 0.16078933, 0.07099252, -0.11573018, 0.24712858, -0.0659458, -0.037504572, -0.12297423, 0.03342632, -0.058119852) * go_1(0.0, 1.0);\n    result += mat4(-0.020957774, -0.0224927, 0.04069268, -0.07911167, 0.074009344, 0.065916434, 0.008222278, 0.11625076, -0.25299504, 0.03357169, -0.021988, 0.015821831, -0.0021187372, -0.030700417, -0.004374924, 0.027358979) * go_1(1.0, -1.0);\n    result += mat4(0.06549052, -0.048067164, 0.05489091, -0.28851983, 0.13378961, 0.026875904, -0.09877994, -0.19947459, -0.1274035, -0.022928834, -0.26344195, -0.025870804, 0.022505255, 0.0070861108, 0.121051334, -0.025964163) * go_1(1.0, 0.0);\n    result += mat4(0.059426542, -0.0327433, 0.2313695, -0.07046268, 0.20479666, 0.027021704, 0.2564928, -0.11689885, -0.07407976, -0.019611249, 0.093463086, -0.121553615, 0.035009407, -0.008135333, -0.075931996, 0.047803063) * go_1(1.0, 1.0);\n    result += mat4(-0.059434246, -0.1652242, -0.124611154, 0.04743711, 0.10530296, -0.13869187, -0.036534663, -0.035206333, 0.06067593, 0.06126907, 0.120151915, -0.06722673, 0.008103894, 0.037225723, -0.007520425, 0.065720856) * go_2(-1.0, -1.0);\n    result += mat4(-3.6759695e-05, -0.036789574, 0.013370567, -0.037871476, -0.013454664, 0.15086569, 0.10164699, 0.057703357, -0.12871023, 0.12827681, -0.055057358, -0.040753044, -0.0142621, 0.08563361, -0.04615499, -0.03130452) * go_2(-1.0, 0.0);\n    result += mat4(-0.117965914, 0.09056485, 0.07272314, 0.009695964, -0.11331058, 0.07467256, -0.08291521, 0.00937355, -0.04097737, 0.07752905, -0.017335521, -0.12539999, 0.039462104, -0.0007037007, 0.06034812, -0.09497377) * go_2(-1.0, 1.0);\n    result += mat4(0.20828065, 0.0400099, 0.047638226, -0.046423353, -0.026133502, 0.098207295, 0.056742374, 0.017029466, -0.058164768, -0.046973787, -0.17328712, -0.0012984811, 0.050085854, 0.11296557, 0.12639083, 0.058543045) * go_2(0.0, -1.0);\n    result += mat4(-0.098907426, 0.22031747, 0.101559944, 0.06616554, 0.026110496, 0.56487054, 0.23754556, -0.07540935, 0.31768414, -0.47653618, 0.015073956, -0.33731326, 0.087285936, -0.24593173, -0.26141426, 0.15003823) * go_2(0.0, 0.0);\n    result += mat4(0.046026446, -0.13767281, 0.064847544, 0.07717139, 0.08544123, -0.11092969, 0.072325274, 0.010849038, -0.3055905, 0.66436774, 0.1434729, 0.0494463, 0.07115603, 0.083811216, 0.020431712, 0.06537088) * go_2(0.0, 1.0);\n    result += mat4(-0.15532711, 0.030139687, 0.040853374, 0.11089222, -0.08150315, -0.015851755, -0.06787692, 0.096075505, -0.011956207, -0.0017758606, 0.1277494, 0.16156575, -0.038588695, -0.0626418, -0.041797023, -0.19467135) * go_2(1.0, -1.0);\n    result += mat4(0.12917455, 0.017410474, -0.20125067, -0.08040003, -0.13494664, 0.17789102, -0.19909395, 0.08441434, 0.078570575, -0.06330619, 0.23767303, 0.5442659, -0.009227878, -0.021818208, 0.14318731, -0.09042824) * go_2(1.0, 0.0);\n    result += mat4(0.097801, 0.09345441, 0.17846581, -0.14773296, 0.06536365, 0.07642184, -0.011880635, 0.02086135, 0.013336972, -0.053295113, -0.13410404, 0.027241753, 0.087728985, -0.044033397, -0.13098569, 0.009423933) * go_2(1.0, 1.0);\n    result += mat4(-0.02488427, 0.0134966355, -0.0075000813, 0.07272353, 0.015842725, 0.13765687, 0.028079558, -0.08384948, -0.06666623, -0.023220664, 0.025091043, -0.055167805, -0.18826278, 0.04423603, 0.13499942, 0.059128854) * go_3(-1.0, -1.0);\n    result += mat4(0.01935146, -0.030980906, -0.031569187, -0.0036869382, 0.036753897, 0.118464164, 0.15871695, -0.09842428, 0.023324292, 0.071796335, -0.07869346, -0.10751301, -0.2588698, 0.064011686, 0.17386378, -0.039197855) * go_3(-1.0, 0.0);\n    result += mat4(0.08590827, 0.005497696, -0.026512025, 0.015661815, 0.1102415, -0.08268483, -0.0032903247, 0.10049029, -0.008157236, -0.035823178, -0.017570151, -0.081716835, -0.3531045, 0.010005245, 0.017141227, -0.016376914) * go_3(-1.0, 1.0);\n    result += mat4(-0.16617337, -0.007689783, 0.00954665, 0.07117733, -0.001669262, -0.012331606, 0.051613946, 0.062780835, 0.06123557, -0.20243123, -0.19181818, 0.032895602, 0.19760677, 0.004464939, 0.12754539, -0.27360034) * go_3(0.0, -1.0);\n    result += mat4(0.15006685, -0.083587274, -0.03215495, -0.16992462, -0.011944293, 0.058361508, -0.088097006, 0.023880545, -0.04168166, -0.06960282, -0.092672385, -0.057278465, 0.23540072, -0.1721208, -0.018213503, -0.23494521) * go_3(0.0, 0.0);\n    result += mat4(-0.124885194, 0.1905868, 0.11108704, 0.03163991, 0.11383064, 0.101223364, 0.069428995, -0.14298953, -0.07609092, 0.13704266, -0.07749446, -0.0005389336, -0.04617235, 0.18011934, 0.08350316, 0.09416366) * go_3(0.0, 1.0);\n    result += mat4(0.073356606, 0.067966126, -0.21285574, 0.0782625, -0.0034364646, -0.032581426, -0.05538558, -0.1317288, 0.14552782, -0.1132393, 0.13063973, -0.00833602, 0.0026844777, 0.028135289, -0.02536825, -0.028372496) * go_3(1.0, -1.0);\n    result += mat4(-0.318728, 0.07862527, -0.12176221, 0.35010242, -0.029198067, 0.016302662, 0.17667587, 0.12605923, 0.1556697, -0.06061443, 0.05843511, 0.10891248, 0.01267106, -0.018492714, -0.15945031, -0.050723754) * go_3(1.0, 0.0);\n    result += mat4(-0.21555941, -0.016813517, -0.084676236, -0.07545412, -0.14518794, -0.014592766, -0.2446481, 0.0530632, 0.0847341, 0.12342537, -0.028644923, 0.083479315, -0.04179012, 0.0025225023, 0.16006976, -0.026940256) * go_3(1.0, 1.0);\n    result += vec4(-0.060742114, -0.037577342, 0.055704296, 0.03134311);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!SAVE conv2d_2_tf\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.13129333, -0.022117995, -0.009753253, 0.020439912, 0.044090994, -0.0916335, 0.0036765633, -0.11719207, -0.06413809, 0.04079378, -0.00085516454, -0.06306388, -0.12660664, -0.054126263, -0.005513979, 0.06364538) * go_0(-1.0, -1.0);\n    result += mat4(-0.028422508, 0.23270117, -0.28674677, -0.10820166, 0.024321957, -0.0811145, -0.07290707, -0.02125165, -0.064260505, 0.052076746, -0.009654081, 0.08363882, -0.02037171, 0.15006389, 0.121593125, -0.011237004) * go_0(-1.0, 0.0);\n    result += mat4(-0.14672333, 0.015381624, 0.1028172, -0.041823238, 0.0072677187, -0.042953942, 0.06426537, -0.0938381, -0.05990813, -0.04599802, -0.11264726, -0.027826328, -0.058160868, 0.10747306, -0.07327458, 0.07998872) * go_0(-1.0, 1.0);\n    result += mat4(-0.08702181, -0.03750975, -0.045659006, 0.04488332, 0.09102003, 0.066556975, -0.04353586, 0.08994567, -0.13561495, -0.10653702, 0.006989605, 0.028230097, 0.07177144, 0.2938447, -0.00943923, 0.022120917) * go_0(0.0, -1.0);\n    result += mat4(-0.1801194, -0.11119162, 0.1977298, -0.247902, -0.16654298, -0.07423158, 0.114130594, 0.0014401592, 0.006954727, -0.09810646, -0.051310766, 0.19487657, 0.2545855, -0.06328558, -0.04617056, 0.09444692) * go_0(0.0, 0.0);\n    result += mat4(0.011378825, 0.16044368, 0.017211074, 0.14472178, 0.032992378, -0.008925819, 0.035120245, -0.012409223, 0.074333005, 0.1178002, -0.128956, -0.13624239, -0.2791275, 0.21457297, -0.1476131, 0.04874687) * go_0(0.0, 1.0);\n    result += mat4(-0.03491764, -0.061763793, 0.05779039, 0.0054837577, -0.023937583, 0.08281698, 0.032306053, -0.014566218, 0.12738499, -0.0132100545, -0.051833414, 0.0057818824, 0.012158851, -0.20231532, -0.0043795826, 0.10285843) * go_0(1.0, -1.0);\n    result += mat4(-0.22269921, -0.15135509, -0.039143335, 0.033390045, 0.06770212, -0.14538582, -0.08011057, 0.03796648, -0.025913516, 0.13925864, 0.18309896, 0.012709204, -0.24912506, 0.3217706, 0.0394195, 0.017977878) * go_0(1.0, 0.0);\n    result += mat4(0.00080196525, 0.059145816, 0.05720508, 0.0056548906, 0.005168018, 0.09938438, 0.0200503, -0.05516137, 0.061309986, -0.019621318, -0.1541441, 0.019540716, 0.030571707, -0.09054893, 0.032851614, -0.27210873) * go_0(1.0, 1.0);\n    result += mat4(0.27061436, -0.114008114, -0.0020118617, -0.1656827, 0.09770587, 0.029897455, -0.03307522, -0.04661818, 0.033011347, 0.18498488, -0.05162084, 0.087471776, -0.24665618, -0.12538423, -0.08123797, -0.010210389) * go_1(-1.0, -1.0);\n    result += mat4(0.075188264, 0.0020608555, 0.18558815, 0.041179713, 0.11232638, 0.05507779, -0.19599183, 0.027942855, 0.06199144, 0.22141005, -0.06121163, 0.014993597, 0.24105869, -0.019737717, -0.112485714, 0.0157406) * go_1(-1.0, 0.0);\n    result += mat4(0.09425698, 0.0207658, 0.12074599, 0.009430481, 0.11889248, -0.025782838, 0.0034711843, 0.05113582, 0.012531833, -0.0018606635, -0.09137569, 0.018120576, 0.4051155, 0.02222076, -0.16001017, 0.10981527) * go_1(-1.0, 1.0);\n    result += mat4(-0.03582557, 0.014994796, -6.4688604e-05, 0.24618183, -0.11697727, 0.24388117, 0.038502026, -0.3511993, 0.101741396, -0.10748137, 0.035059888, -0.017535849, 0.09450039, 0.06541661, 0.12149035, 0.28798738) * go_1(0.0, -1.0);\n    result += mat4(-0.27143848, 0.017990451, -0.69144464, 0.037944376, -0.04551905, 0.09263134, 0.4259611, -0.14107811, -0.10641847, 0.23065196, 0.040813655, -0.07789163, 0.3087666, 0.08190437, 0.16409059, -0.06455426) * go_1(0.0, 0.0);\n    result += mat4(-0.08290655, -0.35286915, -0.18082355, -0.32229406, 0.1608227, 0.030915622, 0.09207708, 0.02655054, 0.039464593, 0.026095424, 0.052584656, 0.033881903, -0.01751319, -0.0011676399, 0.04002607, 0.1630013) * go_1(0.0, 1.0);\n    result += mat4(-0.012021132, 0.12163766, -0.07410629, -0.06879096, 0.017859738, -0.039261997, -0.028677614, -0.23610398, -0.15963873, -0.0006119958, 0.11275506, 0.0082659265, 0.05677582, 0.08676638, -0.08669759, -0.10475464) * go_1(1.0, -1.0);\n    result += mat4(0.12792721, 0.06888765, 0.31803077, 0.26002547, -0.067599155, -0.011822328, -0.2589909, -0.30024147, 0.11076704, 0.15200609, -0.018180368, -0.19146141, 0.22298847, 0.059484895, 0.034478076, 0.15610938) * go_1(1.0, 0.0);\n    result += mat4(0.0870121, -0.016420847, -0.011579898, 0.097182855, -0.120095566, -0.06843338, -0.043460473, -0.060684606, -0.027540063, -0.008499213, 0.033570655, -0.06866259, 0.01429712, -0.07424434, 0.0009466247, 0.09142678) * go_1(1.0, 1.0);\n    result += mat4(-0.03781424, 0.04587032, 0.03744051, 0.02712279, -0.051038064, 0.0669144, -0.02640278, 0.12384894, -0.0022533627, -0.010022036, 0.07536463, -0.030489929, 0.09418577, 0.155089, -0.011290433, -0.02102941) * go_2(-1.0, -1.0);\n    result += mat4(-0.0053278613, -0.07160643, 0.039028414, 0.04123311, -0.10693177, -0.1170874, 0.07230816, -0.033255517, -0.119176835, 0.0786526, -0.11880206, -0.11354601, -0.037539184, 0.14404313, 0.069760695, 0.024738638) * go_2(-1.0, 0.0);\n    result += mat4(0.03413808, -0.006487654, 0.10006853, 0.22228058, -0.13796462, -0.14042488, 0.04017443, -0.031790894, -0.06673143, 0.009888688, 0.08831443, -0.0045771743, -0.028375361, -0.04704813, 0.07128581, -0.07012518) * go_2(-1.0, 1.0);\n    result += mat4(-0.06954315, -0.23728988, -0.14192343, -0.08236467, -0.2552115, 0.04102959, -0.06355397, -0.08340241, 0.17617856, 0.20281969, -0.16249381, 0.10843737, -0.04392261, -0.08587206, 0.053069845, -0.15482199) * go_2(0.0, -1.0);\n    result += mat4(0.124981806, 0.12828638, -0.061472785, -0.20108232, -0.14905351, -0.40766275, -0.35427195, -0.13183996, 0.09307428, -0.07697028, 0.06702549, -0.22656697, 0.019868268, -0.19361132, 0.08784669, 0.20249842) * go_2(0.0, 0.0);\n    result += mat4(-0.004661343, -0.09333453, -0.24876262, -0.07906779, 0.110697776, -0.37069768, -0.042212646, -0.0046135853, -0.2254257, -0.023392014, 0.031476703, -0.045574382, -0.12675518, -0.076056994, -0.08228006, -0.040303517) * go_2(0.0, 1.0);\n    result += mat4(0.16182694, 0.0512523, 0.051189836, 0.048962783, -0.05156489, -0.17987493, -0.012037288, 0.06953726, -0.09458492, 0.1610021, -0.004063283, -0.032922342, 0.08995396, 0.1939926, -0.018710036, -0.08153231) * go_2(1.0, -1.0);\n    result += mat4(-0.064830944, 0.06121252, -0.18886387, -0.12976822, -0.031117212, 0.12219633, 0.19070715, 0.12495262, -0.11994464, -0.24687837, -0.08425294, -0.016920334, -0.13286817, -0.3260188, -0.11776061, 0.1651019) * go_2(1.0, 0.0);\n    result += mat4(-0.17652592, 0.002499805, -0.030541016, -0.01393431, 0.031418208, 0.08209422, 0.12430871, 0.4387016, -0.108871914, -0.09041422, 0.031226631, -0.1638517, 0.20756467, 0.014476537, -0.012701195, -0.03440563) * go_2(1.0, 1.0);\n    result += mat4(0.005320072, -0.0032291536, -0.017209187, 0.031944863, -0.2479921, -0.24433962, -0.13832912, 0.07835928, -0.17707248, 0.028202811, -0.19121435, 0.164587, 0.123152815, 0.0050288937, 0.084104605, -0.0380019) * go_3(-1.0, -1.0);\n    result += mat4(0.16008669, -0.018608516, -0.013778938, 0.033447385, -0.01242472, -0.070916265, 0.026909694, -0.07318777, 0.15158044, 0.12047607, -0.1709358, 0.2031767, 0.0025611701, -0.21457459, 0.2791286, 0.10159932) * go_3(-1.0, 0.0);\n    result += mat4(0.14320926, 0.020023825, -0.0484187, 0.011563084, -0.2640472, -0.013056275, 0.004234292, -0.095376395, 0.28363484, -0.0058227647, -0.0777649, 0.05238444, 0.41757923, -0.07081097, 0.012567031, -0.13029522) * go_3(-1.0, 1.0);\n    result += mat4(0.07266207, 0.042793367, -0.08212271, -0.23401663, -0.19457819, 0.4191269, -0.03095442, 0.15339781, -0.28451788, 0.09316364, 0.10231693, -0.22844811, 0.111623526, 0.120017685, 0.18777381, 0.014420896) * go_3(0.0, -1.0);\n    result += mat4(0.15037206, -0.29763284, 0.2601235, 0.0193363, 0.13686465, 0.009907918, -0.37781665, 0.04916627, 0.14114739, 0.5043813, 0.0447959, -0.029427614, 0.041768756, 0.27211213, 0.14163221, 0.086162075) * go_3(0.0, 0.0);\n    result += mat4(0.19159287, 0.21363218, 0.15053211, 0.08992885, 0.100828275, 0.09379921, 0.030783929, 0.11664482, -0.059145752, -0.19400764, -0.09351283, -0.016430443, -0.12910964, -0.067078374, 0.11760082, 0.121194765) * go_3(0.0, 1.0);\n    result += mat4(-0.055059325, 0.09299572, 0.06848913, 0.06334532, -0.1476285, 0.111801244, -0.033960916, 0.06474366, -0.04952303, 0.27885208, -0.052447475, 0.09226763, -0.15024844, -0.0033919013, 0.013498364, 0.09135676) * go_3(1.0, -1.0);\n    result += mat4(-0.017010042, -0.122343406, -0.19097193, -0.27957183, -0.18206005, 0.102321096, 0.22794476, 0.0439245, -0.23710132, -0.08070259, 0.17377135, 0.23811814, 0.17799385, 0.049567625, 0.1470908, 0.07329385) * go_3(1.0, 0.0);\n    result += mat4(0.0038071256, 0.19454515, -0.01222965, -0.07390379, -0.0532754, 0.03942833, 0.123840906, 0.023459576, -0.0658742, -0.023957543, -0.14682837, 0.1221027, -0.010986398, -0.066184506, 0.03026491, -0.0638446) * go_3(1.0, 1.0);\n    result += vec4(-0.06427697, -0.00039365015, 0.011889719, 0.060232002);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!SAVE conv2d_2_tf1\n//!WIDTH conv2d_1_tf.w\n//!HEIGHT conv2d_1_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.012110923, 0.07818654, 0.07964548, 0.11885079, -0.07694473, -0.01378252, 0.006632789, -0.12876098, 0.0069211307, 0.022278586, 0.069553085, 0.16569804, -0.11123615, 0.06125189, -0.11232848, 0.1559266) * go_0(-1.0, -1.0);\n    result += mat4(-0.3261174, -0.25586754, 0.21129315, 0.3135101, 0.1509055, 0.0044283345, 0.024674175, -0.08000473, 0.01213029, 0.09093019, 0.04942677, 0.09806723, -0.16454464, -0.14433062, -0.058094524, -0.060819894) * go_0(-1.0, 0.0);\n    result += mat4(0.023174008, 0.02858724, 0.07685972, 0.036857616, -0.10415571, 0.10241035, -0.01893166, 0.02065923, 0.058356714, 0.096426114, -0.03772327, -0.1529002, 0.13740575, -0.048291504, -0.06152548, -0.15199897) * go_0(-1.0, 1.0);\n    result += mat4(0.029300174, -0.13222043, 0.0139825605, -0.02274408, 0.062944874, 0.028447356, 0.05960515, 0.034447193, 0.03133432, -0.019283533, -0.024591971, -0.0043914663, 0.15245225, 0.006851478, -0.051783554, 0.17453748) * go_0(0.0, -1.0);\n    result += mat4(-0.09125915, 0.081739366, 0.01196335, 0.23130219, -0.22557035, -0.13537665, 0.0022028848, -0.043430023, 0.22759882, 0.07920754, -0.027986467, -0.14051494, -0.19557038, -0.03585936, -0.4258294, -0.03856216) * go_0(0.0, 0.0);\n    result += mat4(0.18511422, -0.09368415, 0.1551229, 0.04322566, -0.023400841, -0.02261204, 0.15129441, -0.007954805, -0.10739125, 0.019459398, 0.013128325, 0.018073296, 0.20886365, -0.20662378, -0.03814699, -0.09272838) * go_0(0.0, 1.0);\n    result += mat4(-0.027352437, -0.039882626, 0.12598103, -0.093930446, 0.030846786, -0.09325075, -0.009084744, -0.024584265, 0.07159868, 0.14162529, 0.19019091, 0.058855128, -0.09880401, -0.01843218, 0.14753596, -0.2449532) * go_0(1.0, -1.0);\n    result += mat4(0.06565521, 0.09150168, -0.08654865, 0.0829788, -0.07596146, -0.01815166, -0.08786775, -0.03477514, 0.20538878, -0.012766377, 0.020719538, 0.088188395, -0.034300096, 0.29972988, -0.20005241, 0.018425167) * go_0(1.0, 0.0);\n    result += mat4(0.11713916, 0.024167519, 0.05167596, -0.0027117804, -0.016994188, 0.048177514, -0.012556207, 0.010979094, 0.09098878, 0.028514355, 0.06063336, -0.06624107, 0.012754856, 0.013208708, -0.061374772, -0.0025992664) * go_0(1.0, 1.0);\n    result += mat4(-0.09053513, 0.03183455, 0.017340872, 0.12934409, -0.022161964, -0.0015361432, -0.049972344, -0.12763855, 0.12779881, -0.04697911, 0.018968226, -0.119873665, 0.05462772, -0.13919477, -0.10226718, -0.2540179) * go_1(-1.0, -1.0);\n    result += mat4(-0.29912186, -0.09291771, 0.050926663, 0.49361777, 0.21372582, 0.076717265, -0.058968987, -0.1572678, 0.3194591, -0.120582424, 0.03942037, 0.023128232, 0.24321598, 0.07046334, -0.21204855, -0.648296) * go_1(-1.0, 0.0);\n    result += mat4(0.05366883, -0.020366706, 0.020979457, -0.06893884, 0.04837168, 0.017253762, 0.008874203, -0.020785445, -0.20425391, 0.060179923, 0.046167206, 0.09863377, -0.14381303, 0.038928367, -0.06590863, -0.18408588) * go_1(-1.0, 1.0);\n    result += mat4(0.07099762, 0.2029403, -0.033945918, 0.15202214, 0.0901113, -0.27336198, -0.17693861, -0.16206753, -0.17642029, 0.09400492, -0.11165698, -0.07863893, -0.16306102, -0.056210615, 0.22173557, 0.013508989) * go_1(0.0, -1.0);\n    result += mat4(0.08541511, -0.27093616, -0.35273993, -0.48919773, 0.038383547, -0.16013749, 0.012996215, -0.03434873, 0.07024113, -0.28971404, 0.10623425, -0.0019642068, -0.062374946, 0.3291145, 0.22468035, -0.42971882) * go_1(0.0, 0.0);\n    result += mat4(0.020427933, 0.15062793, 0.08308975, -0.025095072, 0.030093266, -0.09649862, -0.03382388, -0.0016017791, 0.105402954, 0.020693144, -0.051065, 0.07704679, 0.02864139, -0.00135146, 0.03762216, 0.029277142) * go_1(0.0, 1.0);\n    result += mat4(0.01700994, 0.12214317, 0.06749582, 0.07354159, -0.093085855, -0.065021954, 0.010773045, -0.00095128635, -0.045384295, -0.072611265, -0.043900184, 0.049471326, 0.029131187, 0.03180158, -0.13313527, 0.05280797) * go_1(1.0, -1.0);\n    result += mat4(0.14751251, -0.15087761, 0.09932281, -0.099232934, -0.062390897, 0.112391844, -0.09159478, 0.15856399, 0.034708973, 0.01819943, -0.02730164, -0.13562973, -0.05687333, -0.0114601655, 0.07025971, 0.02496533) * go_1(1.0, 0.0);\n    result += mat4(-0.0117268525, -0.026162883, 0.07481553, 0.13420302, 0.029870516, 0.07405776, -0.06379041, 0.09631234, -0.07754842, 0.035888605, 0.0034764851, -0.040771756, -0.092022054, -0.034230903, -0.02281844, -0.0028173258) * go_1(1.0, 1.0);\n    result += mat4(-0.059846643, 0.016772347, -0.02287152, 0.07036337, -0.024946844, 0.09826078, -0.068491876, 0.20852126, 0.073890835, -0.058288682, 0.013093785, -0.05776076, 0.0516503, 0.052794468, 0.10837015, 0.038539834) * go_2(-1.0, -1.0);\n    result += mat4(-0.16391893, -0.008062687, -0.35022175, 0.2510062, -0.15820411, 0.048403125, 0.024878092, 0.037888516, -0.035924178, -0.068953894, -0.025386479, 0.24405715, -0.018495679, -0.051277515, 0.14754932, -0.031538483) * go_2(-1.0, 0.0);\n    result += mat4(-0.038429607, -0.047140498, -0.018157095, -0.029318782, -0.04094171, -0.11870087, 0.11214255, 0.07142628, 0.021007229, -0.005681072, 0.1662777, 0.10829575, 0.112268396, 0.03567479, -0.06738845, 0.0032037434) * go_2(-1.0, 1.0);\n    result += mat4(-0.032217573, 0.2102397, -0.20617546, -0.07920811, 0.12918773, 0.054486286, -0.13656865, 0.05806265, 0.01963165, 0.049910642, 0.15538268, 0.10724465, -0.09697837, -0.03070673, -0.0071386313, -0.11899626) * go_2(0.0, -1.0);\n    result += mat4(0.130827, 0.0051715383, -0.07212691, 0.45726067, 0.2773031, 0.2973666, 0.3951691, 0.01333662, -0.14561643, 0.04508669, 0.121690124, 0.13326228, -0.22579186, 0.058161184, 0.09281702, -0.00079749606) * go_2(0.0, 0.0);\n    result += mat4(-0.00771113, 0.09912341, -0.41895548, -0.06705759, 0.029148718, 0.052991726, 0.18665347, -0.031787418, 0.23053595, 0.09444956, 0.10691037, -0.06325714, -0.05335701, 0.1917427, -0.0065284846, 0.032622546) * go_2(0.0, 1.0);\n    result += mat4(-0.056801565, -0.019131258, -0.0939022, -0.08130343, -0.11051993, 0.0035269214, -0.047361933, -0.0543875, 0.10854369, 0.06445185, 0.016828364, -0.022595318, 0.1450623, 0.033027507, -0.020425137, 0.16169788) * go_2(1.0, -1.0);\n    result += mat4(-0.08747717, 0.07770065, 0.018155783, 0.07160794, 0.09860347, -0.04329888, -0.0043579484, -0.2014418, -0.060260013, 0.0036374568, -0.17566042, -0.2268221, 0.001273691, -0.2609373, -0.19417606, -0.04102927) * go_2(1.0, 0.0);\n    result += mat4(-0.086845055, -0.114253804, -0.13433142, -0.025941795, -0.0155711295, -0.13578776, 0.12059696, -0.08760523, -0.0057348222, 0.12164273, 0.07270617, -0.06352636, 0.08894258, 0.04140841, 0.1230304, -0.030357126) * go_2(1.0, 1.0);\n    result += mat4(0.03320213, 0.015911903, -0.06288296, -0.121976145, 0.2713457, 0.13913193, -0.092420585, 0.105714336, 0.10294281, -0.04591945, -0.11767934, 0.032249406, -0.06506192, -0.04639334, 0.08137017, -0.031746846) * go_3(-1.0, -1.0);\n    result += mat4(0.13717805, 0.0071242675, -0.077256985, -0.14974317, -0.08467893, -0.20126395, -0.06240603, 0.09554399, -0.075844854, 0.28380412, 0.046030026, 0.053188596, 0.50943077, 0.1179795, 0.32203588, -0.06712207) * go_3(-1.0, 0.0);\n    result += mat4(-0.18528835, 0.0016975187, -0.0041140947, 0.11234392, -0.34049067, -0.056880493, -0.04325441, 0.09905571, 0.10978758, 0.009608353, -0.10801905, -0.04071131, -0.09096832, -0.12350487, 0.011801418, 0.22521795) * go_3(-1.0, 1.0);\n    result += mat4(0.040283076, -0.034117915, -0.026142653, -0.06058959, 0.12511659, 0.4131219, 0.59190845, 0.39758852, 0.16032091, -0.5975032, -0.14516282, 0.115154505, 0.03874097, 0.18462797, 0.22934213, 0.05285643) * go_3(0.0, -1.0);\n    result += mat4(-0.17804009, 0.33769128, -0.14572927, -0.029545018, 0.3897, -0.055615567, 0.15232995, 0.48788264, -0.21422523, 0.03397293, 0.0337794, -0.19830915, -0.022457365, -0.35096076, 0.42616987, -0.19268763) * go_3(0.0, 0.0);\n    result += mat4(-0.13191561, -0.18337126, 0.017879983, -0.070472844, -0.09409196, -0.025770849, -0.060219247, 0.10869267, -0.17341033, -0.09199785, -0.0667796, -0.093538545, -0.21300837, 0.030474098, -0.04540468, 0.041321553) * go_3(0.0, 1.0);\n    result += mat4(-0.0998177, -0.08669185, -0.0090886615, 0.0021083376, 0.08900095, 0.5062186, 0.45537788, 0.029077586, -0.1001008, -0.0077697043, -0.0096318, 0.11706454, 0.07401959, -0.00650215, 0.06092762, 0.037442297) * go_3(1.0, -1.0);\n    result += mat4(-0.18500404, 0.0024998419, -0.11761331, -0.026825588, 0.27255726, 0.093010515, 0.3281413, -0.051473666, -0.050259475, -0.17258662, -0.23394547, 0.104795866, 0.035074063, -0.061560635, 0.05975411, -0.094255395) * go_3(1.0, 0.0);\n    result += mat4(-0.023440497, -0.021479638, 0.0036277648, 0.004972212, 0.02416659, -0.09856867, -0.03971455, -0.27094853, 0.026615402, -0.0047890246, -0.13755885, 0.16591635, -0.0016293586, 0.133207, 0.047790572, 0.029041538) * go_3(1.0, 1.0);\n    result += vec4(-0.0063728676, -0.029053684, -0.052831043, 0.006475641);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!SAVE conv2d_3_tf\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.0431447, 0.047972627, 0.09522898, 0.19048582, 0.0015511789, 0.1182684, -0.065335006, 0.061233886, -0.02451869, 0.065670215, -0.015341636, 0.06836347, 0.10215459, 0.17516296, 0.0857072, 0.072732896) * go_0(-1.0, -1.0);\n    result += mat4(0.10117189, 0.049022958, -0.016017418, -0.12119866, 0.089112304, 0.016286526, -0.025251161, 0.03239003, -0.0783818, -0.086096615, -0.13673106, -0.15934734, -0.51308054, -0.061430074, -0.16208844, 0.2227776) * go_0(-1.0, 0.0);\n    result += mat4(-0.011567444, 0.025550444, -0.018439503, -0.015003767, 0.11606929, -0.11613111, -0.040906087, -0.015202219, 0.03932618, -0.1106059, 0.03703376, 0.018548314, -0.12761284, -0.038109995, -0.23577367, 0.20272344) * go_0(-1.0, 1.0);\n    result += mat4(0.025444161, -0.075270735, 0.10999789, 0.16305386, 0.016178958, -0.074034974, 0.1177035, -0.077481024, -0.047774278, -0.029782977, 0.23137823, -0.2389453, 0.033015423, -0.10381626, -0.16437943, 0.20906886) * go_0(0.0, -1.0);\n    result += mat4(-0.098473966, 0.11013442, -0.18486807, 0.1907086, -0.17564997, -0.08509439, -0.42472756, -0.17446618, 0.3440862, 0.12719585, -0.12213955, -0.02246555, 0.18982963, 0.20809166, -0.36067408, 0.51116616) * go_0(0.0, 0.0);\n    result += mat4(-0.019805575, 0.07812505, 0.061653323, -0.08379226, 0.026396899, 0.009063019, -0.10845824, 0.0827647, 0.045301896, -0.07748021, -0.07435832, 0.14860612, -0.077515624, 0.010588131, -0.22704287, 0.26849246) * go_0(0.0, 1.0);\n    result += mat4(-0.02884339, -0.09512523, -0.038564682, 0.08862835, 0.041666254, -0.10532901, 0.040582962, -0.10063983, -0.15736029, -0.03644334, -0.005061672, 0.04302295, -0.046482194, -0.05262547, 0.05110866, 0.03204655) * go_0(1.0, -1.0);\n    result += mat4(-0.005932702, 0.033263832, 0.0044865874, -0.02328917, 0.056534443, -0.14084046, 0.022353357, 0.015087431, -0.2734596, -0.026544483, 0.06297078, 0.11277746, 0.06127936, 0.02466357, -0.04970561, 0.02098484) * go_0(1.0, 0.0);\n    result += mat4(0.013603583, 0.036264602, 0.10985147, 0.01532773, -0.09012781, 0.1132652, -0.17016481, 0.025332611, -0.077462606, 0.02990799, -0.10627784, -0.006231141, -0.089164406, -0.051507175, -0.043900985, 0.09049239) * go_0(1.0, 1.0);\n    result += mat4(-0.15391691, 0.1915742, 0.014101639, -0.022153432, 0.06291936, -0.017871676, -0.016763045, -0.14741553, -0.011252563, -0.20720159, -0.030648025, -0.0142307645, 0.010291614, -0.09243969, -0.052940153, 0.0061574522) * go_1(-1.0, -1.0);\n    result += mat4(0.032283742, 0.030768922, 0.1070225, -0.027818602, 0.10032608, 0.0061178426, -0.03561339, -0.26687133, 0.14369439, -0.11362691, -0.08980895, 0.066520914, 0.33414948, 0.006998835, 0.09193012, -0.2857383) * go_1(-1.0, 0.0);\n    result += mat4(-0.059588976, -0.02046844, -0.042585023, 0.031939838, 0.12796514, -0.06155685, 0.03540324, 0.009929082, -0.0039611827, 0.10790477, 0.049435645, -0.083034374, 0.23874004, -0.07460337, -0.020173345, -0.2006587) * go_1(-1.0, 1.0);\n    result += mat4(-0.13217632, 0.052319963, -0.026713084, -0.0051368694, -0.10380872, -0.28659084, 0.0044393227, 0.005174543, -0.05092618, -0.07092548, -0.027397033, -0.01609789, 0.13699281, -0.14706929, 0.17737861, -0.23746766) * go_1(0.0, -1.0);\n    result += mat4(0.19268502, 0.14133929, -0.1305119, -0.4034132, 0.057504695, -0.24550998, -0.081932545, 0.45489246, -0.29331785, 0.19625074, 0.063166246, 0.15158689, 0.6715147, -0.4610189, 0.08921431, 0.17761138) * go_1(0.0, 0.0);\n    result += mat4(0.044718128, -0.011809122, 0.024131307, -0.30093196, -0.05607289, 0.047759805, 0.004210022, 0.098192796, 0.030430846, 0.008207501, 0.12266905, -0.10549182, 0.11584339, -0.091016166, -0.08635591, -0.13889709) * go_1(0.0, 1.0);\n    result += mat4(-0.19226642, 0.07147627, -0.14759602, 0.4041079, 0.0744628, -0.19612685, 0.1498252, -0.06273549, 0.017959936, 0.10858338, -0.14985329, 0.062042814, -0.13240446, -0.24362786, 0.113626175, -0.15332204) * go_1(1.0, -1.0);\n    result += mat4(0.08383099, -0.13935047, -0.25981048, 0.16491203, 0.07513876, -0.28346774, 0.19722275, -0.044425573, 0.020889329, -0.22140723, 0.025403097, -0.09183192, 0.014202567, -0.18666178, 0.062913105, -0.047674105) * go_1(1.0, 0.0);\n    result += mat4(-0.1862771, 0.25878942, -0.043018065, 0.22144824, 0.016088247, 0.12113542, -0.11965952, -0.01587184, 0.07830932, -0.16069177, 0.13421321, 0.018718706, 0.09548377, 0.018543294, 0.013614677, -0.1054485) * go_1(1.0, 1.0);\n    result += mat4(-0.2121733, -0.015635416, 0.027564054, -0.085904464, 0.064805664, -0.070543915, 0.08966146, -0.06359783, 0.01131311, 0.046913184, -0.09809833, -0.092063695, -0.087217696, 0.012411829, 0.0045399712, 0.027389864) * go_2(-1.0, -1.0);\n    result += mat4(-0.19307798, 0.09449126, 0.084036835, 0.30262446, 0.011706106, 0.029800637, 0.04612629, 0.006186647, 0.11228541, 0.055147965, 0.17659879, -0.023410015, 0.19965266, -0.06684007, -0.081968054, -0.052410994) * go_2(-1.0, 0.0);\n    result += mat4(-0.058564443, 0.08252549, 0.058217794, 0.0864448, -0.25663558, 0.080260284, -0.0010294432, 0.05830051, -0.07684524, 0.1820709, 0.04438993, 0.019178499, -0.12425012, -0.04596089, -0.010032888, -0.0012803525) * go_2(-1.0, 1.0);\n    result += mat4(-0.43352658, 0.15262963, 0.25620222, 0.22428556, 0.09667152, 0.0037820593, -0.07951691, -0.11553085, 0.12982155, 0.17988266, -0.14283511, 0.074744284, 0.03604327, 0.00452661, -0.12865154, -0.020020623) * go_2(0.0, -1.0);\n    result += mat4(0.06850602, -0.18057181, 0.2093389, -0.07333886, 0.28406742, -0.048766967, 0.18114483, 0.47292945, -0.2340266, -0.06862712, 0.28263155, 0.3150323, -0.054724697, -0.16958356, 0.27928987, -0.19666018) * go_2(0.0, 0.0);\n    result += mat4(0.03281329, 0.0038649621, -0.07108877, 0.10791149, 0.15235375, -0.3083721, 0.168294, 0.10379698, 0.029038485, 0.16282903, 0.04483725, -0.018684763, 0.108186625, 0.027885616, -0.019351846, 0.1623065) * go_2(0.0, 1.0);\n    result += mat4(-0.110499054, 0.31347123, 0.030852, 0.01631416, -0.1466389, 0.080429435, -0.18689284, 0.10667815, 0.20645237, -0.18004708, -0.10570413, -0.15435064, -0.019000605, -3.126077e-06, 0.037761535, -0.015040956) * go_2(1.0, -1.0);\n    result += mat4(-0.023364332, -0.023399066, 0.2712722, 0.049637552, -0.10222765, -0.2698945, 0.20991959, 0.04921932, 0.21510898, -0.0751939, -0.19781734, -0.28162366, -0.041881047, 0.0065111094, -0.04102195, 0.0982682) * go_2(1.0, 0.0);\n    result += mat4(-0.032176614, 0.019144032, -0.08985387, 0.091637276, 0.1012352, 0.0003583357, 0.07897295, -0.09531175, -0.001155058, 0.074372366, -0.026186578, 0.07283374, 0.06052053, 0.009307753, -0.03874333, -0.06228009) * go_2(1.0, 1.0);\n    result += mat4(-0.022224072, -0.15717922, -0.1406057, -0.05941157, -0.028769474, -0.21226564, -0.036570027, 0.22266355, 0.14120889, 0.014577123, 0.10216447, 0.018429281, 0.056729726, -0.055834044, 0.058146577, -0.11999068) * go_3(-1.0, -1.0);\n    result += mat4(0.009995364, -0.020045493, -0.0057422677, 0.0643022, 0.016475432, -0.030856136, 0.042140726, 0.15077904, -0.32955253, 0.0694449, 0.17931722, 0.3439302, -0.12484157, -0.10958869, -0.15755124, -0.09755644) * go_3(-1.0, 0.0);\n    result += mat4(-0.008314924, 0.07704758, 0.043228816, -0.08110893, 0.099286236, -0.053224478, 0.22877018, -0.189486, -0.00798416, 0.018341504, 0.10734141, 0.0752633, -0.042524844, -0.086395286, 0.14299925, 0.026488977) * go_3(-1.0, 1.0);\n    result += mat4(-0.052531082, 0.19139186, 0.12205995, -0.2573172, 0.15157184, 0.0073150825, 0.089774385, 0.06604469, -0.16528498, -0.002511137, 0.14287429, -0.07819732, 0.025014274, 0.15338829, 0.0761692, -0.02803716) * go_3(0.0, -1.0);\n    result += mat4(-0.21000335, 0.15277153, 0.08546171, 0.2816124, -0.16559112, -0.11068559, 0.47053605, -0.009787771, -0.0013089112, -0.06985127, 0.44743782, 0.25142467, -0.32670796, 0.044035822, -0.12545367, -0.2996084) * go_3(0.0, 0.0);\n    result += mat4(-0.11526387, 0.15654811, 0.099616654, 0.15473685, 0.21278231, 0.046207245, 0.117993094, -0.26825273, -0.12539764, 0.14013724, 0.17357737, -0.05387817, 0.076738276, -0.13339446, 0.15005626, -0.2108176) * go_3(0.0, 1.0);\n    result += mat4(-0.0008846504, -0.05998622, -0.028892396, 0.04784136, 0.0104263965, 0.10899508, -0.073364735, 0.077516064, -0.074248806, -0.21749993, -0.26203, 0.041161157, 0.09366407, -0.026498007, 0.0122177545, 0.03892727) * go_3(1.0, -1.0);\n    result += mat4(0.04349908, 0.13671173, 0.2242545, -0.028021423, -0.03802222, 0.0052366396, -0.010709643, 0.031290106, 0.06291333, -0.024909683, -0.15439379, -0.04502091, 0.2062182, -0.5983536, -0.09670497, -0.38446042) * go_3(1.0, 0.0);\n    result += mat4(-0.008962513, 0.13044207, 0.04964221, 0.012250417, 0.012129821, 0.019985713, -0.06421885, 0.009168735, -0.044516414, 0.071368866, -0.006634213, 0.06497366, 0.08578495, -0.10586125, 0.06628038, -0.14006054) * go_3(1.0, 1.0);\n    result += vec4(0.056541316, 0.041788545, -0.036094554, -0.021763096);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!SAVE conv2d_3_tf1\n//!WIDTH conv2d_2_tf.w\n//!HEIGHT conv2d_2_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.0647927, 0.053666476, -0.14723225, 0.027874574, -0.0003166473, 0.07337155, -0.061972085, -0.012667777, -0.17071614, 0.091927536, -0.051160213, 0.21336353, 0.13854574, 0.09582817, 0.032316446, 0.13838023) * go_0(-1.0, -1.0);\n    result += mat4(-0.0398984, 0.108049214, 0.093780346, -0.022015186, -0.15188989, -0.1381083, 0.2998843, 0.21623154, -0.08862326, 0.025862623, 0.06895634, 0.13529755, 0.06957801, -0.0011681129, 0.105972745, -0.04722446) * go_0(-1.0, 0.0);\n    result += mat4(-0.026321493, -0.04828038, -0.012545767, -0.005490858, -0.054038163, 0.075943105, -0.11526662, 0.022242405, -0.03543104, -0.12451852, -0.14911178, 0.013503498, 0.08773292, 0.09695139, -0.013498657, -0.27424073) * go_0(-1.0, 1.0);\n    result += mat4(0.018575635, -0.11321618, -0.07853153, 0.04104883, 0.0018416744, 0.11579002, 0.03685964, -0.031546146, -0.1755398, 0.23517849, -0.08095411, 0.031999595, -0.18542038, -0.26171613, -0.20567231, -0.05683613) * go_0(0.0, -1.0);\n    result += mat4(0.1538556, 0.21723682, 0.12131733, -0.15308167, 0.103326, -0.006956118, 0.043583486, -0.23811384, -0.103285454, 0.05543916, -0.37894246, 0.32072112, 0.22651967, 0.03516268, 0.34612176, 0.23688535) * go_0(0.0, 0.0);\n    result += mat4(0.040021293, 0.0029912095, 0.04885362, 0.061496444, 0.016926387, -0.118446946, 0.038948335, -0.0934512, -0.25194243, -0.054018084, -0.07149527, 0.017903058, 0.0845516, 0.33802906, 0.11953944, -0.081294954) * go_0(0.0, 1.0);\n    result += mat4(-0.09558082, -0.36974236, -0.07524102, 0.11131445, 0.047626104, 0.12854609, -0.10264962, -0.044669047, -0.05572307, 0.34475142, -0.16806377, -0.0037204176, 0.03400533, -0.04047774, 0.024379745, 0.09056291) * go_0(1.0, -1.0);\n    result += mat4(-0.039392482, 0.2553437, 0.11705501, 0.03219211, 0.073977776, -0.16610906, -0.032796364, -0.054669864, -0.07123178, 0.00079619256, -0.36920992, -0.029054813, 0.12830003, 0.004987549, 0.08724278, -0.029499404) * go_0(1.0, 0.0);\n    result += mat4(0.021272454, -0.063295126, 0.011779576, 0.103093, -0.011095461, 0.027948728, -0.014605259, -0.04723974, -0.05334346, -0.044831257, -0.07296399, -0.03314197, -0.01687865, -0.09261895, -0.06128567, 0.092708185) * go_0(1.0, 1.0);\n    result += mat4(0.0077418387, 0.00871427, 0.060824487, 0.1093608, -0.021077013, -0.057341542, -0.04769576, -0.08144089, 0.0212823, -0.06731425, -0.04134463, -0.0016761447, -0.03402026, 0.036424547, 0.11689576, -0.14946719) * go_1(-1.0, -1.0);\n    result += mat4(0.18536687, 0.020073935, 0.17041959, 0.024790209, 0.08397728, -0.13884324, 0.013950321, -0.055075396, -0.09317963, -0.05723721, -0.060491834, 0.0017911601, -0.109154835, 0.010338362, -0.1982491, -0.21752335) * go_1(-1.0, 0.0);\n    result += mat4(0.031852514, 0.031424347, 0.07817056, 0.07770759, 0.019805199, -0.091223724, 0.11914662, 0.1673029, -0.018734453, 0.16275099, 0.23245652, 0.36139074, -0.1396047, -0.14774057, 0.13756078, -0.123794965) * go_1(-1.0, 1.0);\n    result += mat4(-0.034937833, 0.20777488, 0.10104809, -0.035140667, 0.2536575, 0.010970045, 0.16896339, -0.081219964, -0.062478427, -0.0010431948, -0.027980985, 0.11446318, -0.127309, 0.21002083, 0.044436257, -0.16986957) * go_1(0.0, -1.0);\n    result += mat4(0.06309646, -0.042341243, 0.36642808, 0.18653205, 0.06973023, 0.06315932, -0.323688, 0.25672218, 0.042820994, 0.13792914, -0.12892757, -0.09220378, -0.18939693, 0.03862022, -0.17376114, -0.24673308) * go_1(0.0, 0.0);\n    result += mat4(-0.02130602, -0.35428852, -0.011634983, -3.9823462e-05, 0.110818714, -0.2981158, 0.060209107, 0.012538829, -0.0744833, -0.050204318, -0.12676497, -0.031484153, -0.28799182, 0.22338839, -0.070876874, -0.02102363) * go_1(0.0, 1.0);\n    result += mat4(-0.07929991, 0.014598492, 0.23034762, 0.024872296, 0.07480494, -0.17139243, -0.014421178, 0.056448363, -0.028626937, -0.022152562, 0.044871796, -0.048653606, 0.009350802, 0.019022083, -0.08554845, -0.0922645) * go_1(1.0, -1.0);\n    result += mat4(-0.027405115, 0.1831188, 0.28516722, 0.19882526, 0.27299204, -0.06910511, 0.03244419, -0.0031333128, 0.061055277, -0.114398144, 0.03729459, -0.07840815, -0.37776002, -0.24129418, -0.54815483, -0.2702045) * go_1(1.0, 0.0);\n    result += mat4(0.053723935, 0.13472083, 0.09563273, 0.19009806, -0.18722993, -0.25939655, -0.016197463, -0.067061596, 0.1647598, 0.061905228, 0.06191816, -0.018582113, -0.07218153, 0.11278394, 0.05478068, -0.104871586) * go_1(1.0, 1.0);\n    result += mat4(0.0036616288, -0.045782693, -0.226954, -0.05043515, -0.078096785, -0.036197383, 0.09269631, 0.016823346, -0.0060579977, -0.041455746, 0.09032774, -0.09217121, 0.058089796, 0.060311552, 0.033079024, 0.022586476) * go_2(-1.0, -1.0);\n    result += mat4(0.0436363, -0.079482526, 0.0027447809, 0.039558932, 0.13275702, 6.898711e-05, -0.21961488, -0.11315821, 0.0076181027, -0.025279062, -0.15829584, -0.063141204, 0.062049046, 0.13117202, -0.02435016, 0.109555416) * go_2(-1.0, 0.0);\n    result += mat4(-0.010148116, 0.056620967, -0.015910713, -0.07370375, 0.1529919, 0.005792597, 0.02771225, -0.17027487, 0.096740395, 0.063347995, 0.17823112, 0.054105148, 0.04995114, -0.28613812, 0.06369567, 0.15978208) * go_2(-1.0, 1.0);\n    result += mat4(-0.13688345, 0.16967694, -0.061759472, 0.013682004, -0.1290496, 0.07167547, -0.065592445, -0.17897636, 0.057080988, 0.035630587, 0.09140394, -0.08695068, 0.16807681, 0.014749346, 0.07875138, 0.034913708) * go_2(0.0, -1.0);\n    result += mat4(-0.098915346, -0.31459075, -0.10892429, 0.1557498, -0.19764107, -0.26881596, -0.03589311, 0.45288458, -0.34171388, 0.12675741, 0.18415868, -0.19770056, 0.29025507, -0.15812592, 0.09685835, 0.0027761247) * go_2(0.0, 0.0);\n    result += mat4(0.06425249, -0.01169722, 0.06379363, 0.053835012, -0.07356561, -0.06367294, 0.108630784, -0.14137438, 0.08536725, -0.03209748, 0.07250959, -0.014214082, 0.07170588, -0.25647813, 0.1092683, 0.18791042) * go_2(0.0, 1.0);\n    result += mat4(-0.023783233, 0.14261739, 0.102011986, -0.03633555, -0.05032627, 0.09378387, 0.11764051, 0.1353335, 0.032817088, -0.1352964, -0.00667997, -0.13388929, 0.022861317, 0.0037358075, 0.018605746, -0.0009892831) * go_2(1.0, -1.0);\n    result += mat4(0.22419162, -0.23105696, -0.09900454, -0.15831396, 0.12398773, 0.097933106, -0.13189293, 0.1330756, -0.19673057, -0.037342317, -0.13462654, -0.08974021, 0.030326528, -0.0815862, -0.118352115, 0.009187904) * go_2(1.0, 0.0);\n    result += mat4(-0.012130391, -0.06408448, 0.13710785, -0.06678414, -0.09970725, -0.14895032, -0.02366641, 0.029581001, -0.07101809, 0.09414698, 0.018300869, 0.009139046, -0.0027311493, -0.2359952, -0.011602826, -0.007582444) * go_2(1.0, 1.0);\n    result += mat4(-0.15473361, -0.06868751, -0.030721204, -0.08650113, 0.071349874, -0.08177769, 0.1611948, 0.18305337, -0.0144878505, 0.10975452, -0.026968453, -0.04909913, -0.059665974, 0.056036238, -0.11623168, -0.10584912) * go_3(-1.0, -1.0);\n    result += mat4(-0.096973225, 0.054132458, -0.010600018, 0.089397885, -0.0031138035, 0.037452973, 0.041115325, 0.1924831, 0.14759748, 0.032560788, -0.082884625, 0.0324635, -0.083511285, -0.050381303, 0.025589975, -0.0981257) * go_3(-1.0, 0.0);\n    result += mat4(-0.09183111, 0.034952193, -0.048511654, 0.020719057, 0.1863456, 0.01902738, 0.14455654, -0.008500172, 0.16385981, -0.07806569, -0.031216217, -0.17002788, -0.08882952, 0.07335293, -0.2223089, 0.01706056) * go_3(-1.0, 1.0);\n    result += mat4(-0.08361569, 0.046698716, -0.016646344, 0.09351987, 0.0054158634, -0.13641126, -0.12396605, 0.011380122, 0.040951792, -0.11222528, -0.0031548145, -0.0022303525, 0.0350846, -0.03280425, -0.09972476, -0.113325305) * go_3(0.0, -1.0);\n    result += mat4(-0.19961461, -0.27561286, -0.12783135, -0.062596925, 0.005870981, -0.24796526, 0.18717633, -0.16945636, -0.076396205, -0.08411448, 0.13751988, 0.21014418, -0.008655945, -0.09848541, -0.14536901, -0.2132181) * go_3(0.0, 0.0);\n    result += mat4(0.14118621, 0.20831147, -0.020545695, 0.008340737, 0.016840864, -0.16912372, -0.121718146, 0.15108089, -0.19803092, -0.07827729, -0.047639225, -0.12277847, 0.04974115, -0.09349339, -0.2756667, -0.19581003) * go_3(0.0, 1.0);\n    result += mat4(-0.0036992705, 0.16539848, 0.022026122, 0.07740234, -0.035687633, -0.004568715, 0.017408118, -0.09757294, -0.094941914, -0.3381112, -0.12724453, 0.025583982, -0.18571027, 0.047607586, -0.0704089, -0.055323426) * go_3(1.0, -1.0);\n    result += mat4(0.13821335, 0.028168043, 0.09990671, -0.032266147, -0.067236245, 0.11512147, -0.112986445, -0.10818019, -0.10062181, 0.21276556, 0.01681818, 0.069806606, 0.09628121, 0.06456379, 0.10394843, -0.02343886) * go_3(1.0, 0.0);\n    result += mat4(0.041937463, 0.072631165, 0.045366894, -0.0046993676, 0.03946691, 0.121010706, -0.030089365, -0.007266469, 0.0092267515, 0.14853416, -0.033248078, -0.027284347, -0.10031526, 0.15864117, -0.16782752, -0.18466589) * go_3(1.0, 1.0);\n    result += vec4(0.07722432, -0.025165567, 0.034291282, -0.09902708);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!SAVE conv2d_4_tf\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.004729794, -0.0124398535, -0.08538641, -0.058604605, 0.008671952, 0.25604513, 0.020800482, 0.24144122, -0.028920606, -0.04705229, 0.030192787, 0.0010597534, 0.017666103, 0.0041322373, 0.20027764, 0.08919112) * go_0(-1.0, -1.0);\n    result += mat4(0.0001626656, 0.05816014, -0.0060765734, 0.08811165, 0.35835367, -0.016291425, -0.56892496, 0.083845764, 0.15026698, -0.15916558, 0.08069463, -0.3931291, -0.0123534845, -0.111639686, -0.14637001, -0.08171439) * go_0(-1.0, 0.0);\n    result += mat4(-0.114976816, 0.023376396, 0.13855027, 0.07438716, -0.069991484, 0.20377779, 0.23929878, -0.040769435, 0.018832395, 0.005638609, -0.091848075, 0.027843866, 0.023744943, -0.06620523, -0.11678267, 0.0844119) * go_0(-1.0, 1.0);\n    result += mat4(0.0035854098, -0.08432094, -0.17799544, -0.10041983, 0.25605857, 0.021009786, 0.030499447, -0.09928291, 0.052178737, -0.08286175, -0.057888374, 0.024606042, 0.046342995, 0.13875343, 0.11279266, 0.19826262) * go_0(0.0, -1.0);\n    result += mat4(-0.016232021, -0.21539623, 0.0936961, 0.021143785, 0.094262615, 0.049040064, 0.40978724, 0.15347758, 0.08884813, -0.24887115, -0.14756748, -0.5020875, 0.112477, 0.1466549, -0.33418837, 0.5769466) * go_0(0.0, 0.0);\n    result += mat4(-0.16832942, -0.07354198, -0.12081261, -0.055348314, 0.39716053, 0.25583258, 0.09870877, 0.2151021, -0.025700683, -0.1801462, -0.04616654, -0.02782245, -0.054461803, -0.00042802413, -0.00163228, -0.004240747) * go_0(0.0, 1.0);\n    result += mat4(-0.05193433, -0.0018198475, -0.17647028, -0.19462106, 0.1538165, 0.054894235, 0.12183955, 0.07340974, -0.0019901982, 0.0357373, -0.07597063, -0.06681543, -0.00090057997, -0.053894397, -0.010301875, -0.16553953) * go_0(1.0, -1.0);\n    result += mat4(-0.30873474, -0.2836045, 0.057037193, -0.5016378, 0.11952749, 0.102353275, 0.2351629, -0.14635189, -0.019398788, -0.08776502, 0.021669978, -0.089918956, -0.2187901, -0.1180891, -0.049789533, -0.16109149) * go_0(1.0, 0.0);\n    result += mat4(-0.078335494, -0.08867304, 0.03349591, -0.1000293, -0.20235832, 0.22917585, -0.09905303, 0.08381748, 0.014350217, -0.14478815, -0.027479894, -0.026432173, -0.10309177, -0.09860884, -0.019177807, -0.06963025) * go_0(1.0, 1.0);\n    result += mat4(0.008169383, 0.12532842, -0.23369955, 0.077973194, 0.09076616, -0.021277165, 0.1721421, -0.26914293, -0.014729218, -0.023279984, -0.057670787, 0.003598546, -0.015225789, -0.0115396585, -0.26196182, -0.10724508) * go_1(-1.0, -1.0);\n    result += mat4(0.16542235, 0.06589374, 0.07410237, 0.26753154, -0.3356288, 0.3096256, 0.07112498, -0.0992165, 0.15020338, -0.11021673, 0.18803611, 0.12918204, 0.109007336, -0.031968266, 0.057093572, 0.035949256) * go_1(-1.0, 0.0);\n    result += mat4(0.065006174, 0.031055925, 0.0390232, -0.01678507, -0.21553491, 0.14171642, -0.19541772, -0.033691674, -0.06241631, 0.07497651, 0.024557155, 0.056778047, -0.060191352, -0.0261998, 0.07493729, -0.0699132) * go_1(-1.0, 1.0);\n    result += mat4(-0.008541382, 0.020270415, -0.027760057, -0.040962905, -0.26732433, 0.34379438, -0.23012447, 0.0051356517, -0.04059567, 0.0972959, 0.039965224, -0.14796777, -0.0016924662, -0.116963714, -0.026353523, -0.29799464) * go_1(0.0, -1.0);\n    result += mat4(0.03329303, -0.12663862, -0.0004959157, -0.11162377, 0.26238343, 0.43260252, -0.16504994, 0.10727678, -0.22505566, 0.43474057, 0.43304008, 0.05143919, 0.40494493, 0.08689636, -0.035733614, 0.25727916) * go_1(0.0, 0.0);\n    result += mat4(0.12175736, -0.014467151, -0.17461288, -0.18480565, -0.26439998, 0.307935, -0.058916792, -0.014292711, -0.0569471, 0.10751278, -0.04134206, 0.1847734, -0.07519831, -0.033909313, -0.05001451, -0.136606) * go_1(0.0, 1.0);\n    result += mat4(0.1424893, -0.026820501, 0.19645774, -0.0011315406, -0.14680974, 0.07662838, 0.21108222, 0.13260938, 0.17923595, -0.085527614, 0.08217639, 0.06579479, 0.05985784, -0.09016323, 0.11172888, 0.111903176) * go_1(1.0, -1.0);\n    result += mat4(0.19842595, 0.0093640275, 0.10433465, 0.13341904, -0.082806975, 0.22555825, -0.1315717, 0.11907785, 0.24012424, 0.47776055, 0.1835734, 0.17483878, 0.079803735, 0.01155073, -0.21146573, -0.16484722) * go_1(1.0, 0.0);\n    result += mat4(0.15064004, 0.021381427, 0.18301587, 0.21225913, 0.054995645, 0.03212186, 0.052798916, -0.048424408, 0.03609021, 0.0964704, -0.059469886, -0.05133066, -0.08157349, 0.051145166, -0.09107608, -0.1362262) * go_1(1.0, 1.0);\n    result += mat4(0.090521574, -0.014747857, -0.081675015, -0.118686825, 0.04848682, -0.033071827, 0.008534588, 0.023765508, 0.16849907, -0.21797262, -0.17049783, -0.07824179, -0.033794608, 0.052612655, 0.095820345, -0.07262317) * go_2(-1.0, -1.0);\n    result += mat4(0.22816367, -0.13772108, -0.036353834, -0.47638395, -0.0530902, 0.14089061, 0.076203234, 0.18006112, 0.121814854, -0.20750527, 0.08266107, -0.28634354, 0.14301859, -0.13458411, 0.00501663, -0.039783802) * go_2(-1.0, 0.0);\n    result += mat4(-0.103384845, -0.14389835, 0.08275834, -0.068423435, 0.22643796, -0.02966374, -0.2847584, 0.037081387, 0.02349005, -0.19353923, -0.00095957273, -0.13623689, -0.073120415, 0.03941467, 0.21864155, -0.014019576) * go_2(-1.0, 1.0);\n    result += mat4(-0.082576886, 0.17085212, 0.08971252, -0.04213377, -0.032548156, 0.022137715, 0.08399252, -0.0011743539, -0.09410863, -0.41728264, -0.20709297, -0.18933547, 0.027059928, 0.09743364, 0.2504647, -0.041173562) * go_2(0.0, -1.0);\n    result += mat4(-0.20924084, 0.291118, 0.029851688, 0.16953468, 0.02936709, 0.12213576, 0.22944322, 0.108747594, 0.0001881129, -0.27398208, -0.009702691, 0.15449248, -0.9472944, -0.26114875, -0.28161275, -0.3495961) * go_2(0.0, 0.0);\n    result += mat4(-0.12994622, -0.2758638, -0.1091727, -0.0968308, -0.14323105, 0.035175014, -0.08023811, 0.006023802, -0.031529594, -0.1486306, -0.3398172, -0.23240276, -0.29163983, 0.173475, 0.18809283, 0.22197202) * go_2(0.0, 1.0);\n    result += mat4(0.048254848, -0.083444916, -0.014334202, 0.060992356, -0.023099286, -0.09492961, 0.05592045, 0.0026059286, 0.08998117, -0.108810075, -0.053304546, 0.045926623, 0.068255246, 0.099023566, 0.01595483, 0.1336309) * go_2(1.0, -1.0);\n    result += mat4(0.21916585, 0.2837387, 0.14624594, 0.18843961, -0.06747584, 0.054924384, -0.082568415, 0.05011459, 0.014297759, -0.3884833, -0.054417178, -0.18970548, 0.088336475, -0.030646667, -0.2980552, -0.030035203) * go_2(1.0, 0.0);\n    result += mat4(-0.02748568, -0.011897529, -0.2370837, -0.016740574, -0.0282112, 0.050353892, -0.10761107, -0.00036999505, 0.037646662, -0.17742962, 0.06489219, -0.158852, -0.08016933, 0.07808515, -0.105895035, 0.079869986) * go_2(1.0, 1.0);\n    result += mat4(-0.0058994526, -0.037170693, 0.2574696, 0.06199102, -0.04497728, -0.10667442, -0.15183865, 0.0212881, -0.030842574, 0.073473394, 0.010764398, -0.00084518327, -0.03893014, -0.009649613, 0.07443129, 0.15108284) * go_3(-1.0, -1.0);\n    result += mat4(0.11325495, -0.096435815, -0.097331434, -0.049700152, -0.17231967, 0.047090057, -0.019111065, 0.104790315, -0.15004838, 0.13950798, 0.055996202, -0.070548095, 0.047154237, -0.007650949, -0.053611025, -0.012242293) * go_3(-1.0, 0.0);\n    result += mat4(0.12787002, -0.04958212, 0.053988468, 0.0017896162, 0.049493514, -0.009475431, -0.0022641935, 0.03933694, -0.005174597, 0.043754533, -0.1432976, 0.037084177, -0.04601288, -0.032077815, -0.059897035, 0.12584484) * go_3(-1.0, 1.0);\n    result += mat4(0.019409029, 0.10492923, 0.268368, 0.12597778, -0.17733063, -0.0085961, -0.27136415, -0.049664587, 0.012515404, -0.21444482, -0.39275557, -0.12297177, 0.06800057, 0.19228315, 0.06245887, 0.35772634) * go_3(0.0, -1.0);\n    result += mat4(-0.16317715, 0.2288402, -0.23235172, 0.22230752, -0.1646375, 0.13366091, 0.16681044, -0.17399235, 0.33997267, -0.3179832, -0.34756508, 0.39843196, -0.10748536, 0.322923, 0.23339489, 0.08684083) * go_3(0.0, 0.0);\n    result += mat4(0.02835275, 0.12314228, 0.24030593, 0.30856124, 0.055735108, -0.044914473, 0.0031432225, 0.07469899, 0.1778018, 0.107083894, -0.023706734, -0.15501897, 0.0943098, -0.034707237, -0.18622099, 0.05257965) * go_3(0.0, 1.0);\n    result += mat4(0.042839274, 0.12597966, 0.08979042, -0.0647561, -0.050434645, 0.049438696, -0.20008127, -0.05572608, 0.046238814, 0.12622325, -0.019017145, -0.13960391, -0.040050175, 0.14298008, -0.20270552, 0.13391526) * go_3(1.0, -1.0);\n    result += mat4(-0.0073277587, 0.10606624, -0.08940439, -0.09656414, 0.12387374, -0.0013147948, 0.23607181, -0.00037969893, 0.050353236, -0.17266603, 0.27796733, -0.09877832, 0.02711225, 0.096394345, 0.07457944, 0.21541388) * go_3(1.0, 0.0);\n    result += mat4(-0.18612787, -0.00027517386, -0.17136407, -0.06413671, 0.025629476, -0.04570916, 0.0008431566, -0.03419168, 0.08123608, 0.09465922, 0.11975521, 0.1269741, 0.08413221, 0.12125001, 0.04727287, 0.072378494) * go_3(1.0, 1.0);\n    result += vec4(0.04244928, -0.014280219, 0.017129054, -0.08807801);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!SAVE conv2d_4_tf1\n//!WIDTH conv2d_3_tf.w\n//!HEIGHT conv2d_3_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.01973856, -0.05053795, 0.015545361, 0.10867395, 0.33441806, 0.14731607, 0.6793983, -0.21394718, -0.00846322, 0.09146322, -0.07427475, -0.078477465, -0.090998545, 0.133366, 0.105515696, -0.13784988) * go_0(-1.0, -1.0);\n    result += mat4(-0.05404873, 0.09784018, -0.1337389, -0.18082313, 0.13461179, -0.3816801, 0.12209786, 0.08176651, 0.10461896, -0.43315184, 0.017470734, 0.20423968, -0.03941875, -0.101959296, -0.09440259, 0.09154717) * go_0(-1.0, 0.0);\n    result += mat4(0.17229515, -0.06907825, -0.008382803, -0.16671611, -0.01576541, 0.03985307, 0.08209482, -0.11707446, -0.11793074, 0.13702396, -0.02013158, 0.07302033, -0.022301994, -0.11464677, 0.036753565, -0.093276784) * go_0(-1.0, 1.0);\n    result += mat4(-0.017650167, 0.009475923, -0.17856382, 0.15925962, 0.06434641, -0.15568036, 0.038135886, 0.18855911, -0.04427734, 0.1878215, 0.10856261, 0.0041275816, -0.12046199, 0.13610138, 0.3741596, -0.12934728) * go_0(0.0, -1.0);\n    result += mat4(-0.24631616, 0.0169485, -0.035534818, 0.37795424, -0.08546174, 0.07817259, 0.42897213, -0.47965595, -0.0146556785, -0.20510523, -0.18889453, 0.06476019, 0.1021008, -0.35398817, -0.031071864, -0.21416448) * go_0(0.0, 0.0);\n    result += mat4(0.32810766, 0.050585747, -0.17658374, -0.13881154, 0.16417882, -0.21286008, -0.106835455, -0.1722344, -0.14151084, 0.08962986, 0.057395387, -0.01623662, 0.02570415, 0.15626897, -0.12687978, 0.080729105) * go_0(0.0, 1.0);\n    result += mat4(-0.050597478, -0.018753758, -0.036346875, -0.017908493, 0.058593344, 0.008303028, 0.05254987, -0.06635018, -0.022532012, 0.029511122, 0.026682215, -0.054647952, 0.069466785, -0.08892492, 0.025351115, -0.023130694) * go_0(1.0, -1.0);\n    result += mat4(0.2412473, -0.16138165, -0.15117447, 0.11851003, -0.096868426, 0.082690425, 0.27923304, 0.11590443, 0.19363573, -0.15770023, -0.066793665, 0.011681678, 0.14037277, -0.112065665, -0.048159517, 0.009453693) * go_0(1.0, 0.0);\n    result += mat4(0.1580054, -0.0060506654, 0.05267837, -0.09178131, -0.09107123, 0.23191126, 0.21108283, -0.070422985, 0.024321035, 0.06131459, 0.066626504, 0.032481454, 0.044402298, 0.1390604, -0.14432502, 0.040869843) * go_0(1.0, 1.0);\n    result += mat4(0.10264861, 0.013504324, 0.012482852, -0.1781206, -0.12799414, -0.27026084, -0.123830505, 0.098105, -0.039127555, 0.09367889, 0.122323096, 0.1416734, 0.044763107, -0.21801683, -0.14018978, 0.17646866) * go_1(-1.0, -1.0);\n    result += mat4(0.017453065, 0.11498537, -0.10998983, -0.3116098, -0.3099762, 0.5024706, 0.051817298, 0.03170681, -0.18937826, 0.07946567, -0.11978771, -0.09523745, -0.0033551592, -0.11768945, 0.08932359, -0.06689581) * go_1(-1.0, 0.0);\n    result += mat4(0.1507582, -0.013266159, -0.073085934, -0.07252967, -0.06301927, -0.13218755, 0.12984878, -0.13678701, 0.023422396, 0.082123175, 0.006906731, -0.004018426, -0.15813835, 0.13711788, 0.016018609, 0.13443229) * go_1(-1.0, 1.0);\n    result += mat4(-0.06960673, 0.16156524, -0.1374069, -0.05803206, -0.077960715, -0.10676749, 0.26282015, 0.03521529, 0.058099385, -0.014738148, 0.0011174522, 0.24279532, -0.023991548, -0.108812414, -0.08886019, 0.20584475) * go_1(0.0, -1.0);\n    result += mat4(-0.08043308, 0.063343, 0.055290066, -0.15991378, -0.08096304, -0.23888679, 0.019161629, 0.38381267, 0.3672934, -0.119608454, -0.43623593, -0.46014485, -0.5323366, 0.1318621, 0.087373205, -0.05535459) * go_1(0.0, 0.0);\n    result += mat4(0.20640239, -0.1369444, -0.21677823, 0.08202178, 0.10515278, 0.06810837, 0.073207974, 0.23623931, 0.102422275, -0.05016664, -0.0039228587, -0.1810343, -0.2235563, -0.1246854, 0.1428113, -0.10609135) * go_1(0.0, 1.0);\n    result += mat4(-0.031941894, -0.08905056, 0.21501167, 0.11244667, -0.011811734, 0.21630247, 0.07589472, -0.040489636, -0.11824066, -0.11520391, -0.10075633, -0.035642453, 0.062144946, 0.0073282206, 0.14119269, -0.060479023) * go_1(1.0, -1.0);\n    result += mat4(-0.29382935, -0.056808118, 0.051812876, -0.061358813, -0.08344258, 0.124203674, 0.037964176, -0.01961274, -0.000951725, 0.50005037, -0.24176972, 0.06487161, -0.15469861, 0.04336187, 0.17826353, 0.040010225) * go_1(1.0, 0.0);\n    result += mat4(0.02044482, -0.0879271, -0.01053958, -0.31148303, 0.07497373, -0.11548258, -0.1666126, 0.02369657, -0.058044076, 0.010801491, -0.005933901, -0.08910467, 0.007953008, 0.03761974, -0.029501524, 0.16816042) * go_1(1.0, 1.0);\n    result += mat4(0.1779597, -0.10213089, 0.29942423, -0.016642543, -0.015537001, -0.04676146, 0.09585872, -0.0055750017, -0.014361908, -0.20667697, -0.11348746, 0.13081487, -0.10437329, 0.14328459, 0.11648822, -0.09163837) * go_2(-1.0, -1.0);\n    result += mat4(0.019033967, -0.12420627, -0.07748253, 0.43203858, -0.109799065, 0.07605535, 0.060791396, -0.24517195, -0.15674245, 0.21267459, 0.10665515, -0.073150024, -0.1358355, 0.0054066703, -0.16434059, -0.06031853) * go_2(-1.0, 0.0);\n    result += mat4(-0.18834068, 0.26840356, -0.12937617, 0.16103932, -0.0062331813, -0.13630053, -0.013911821, 0.022389365, -0.044232946, -0.056454606, 0.022426741, 0.18010215, 0.041900013, 0.03375041, -0.11376866, -0.010313381) * go_2(-1.0, 1.0);\n    result += mat4(0.12497669, -0.31161824, 0.097568035, 0.19443443, -0.05056519, -0.0031457904, 0.1055554, -0.083650924, 0.07630523, -0.34177595, -0.093093194, 0.20701368, -0.030962149, -0.054470222, -0.23853977, 0.004326528) * go_2(0.0, -1.0);\n    result += mat4(0.34370202, 0.085750066, -0.16071722, -0.54335934, -0.35595295, -0.050744478, -0.17405547, 0.008628697, -0.007086256, 0.23164117, 0.340156, 0.5475976, -0.15292351, 0.28019544, 0.038059216, 0.0044727) * go_2(0.0, 0.0);\n    result += mat4(-0.08231968, -0.0052294536, 0.07451547, 0.22278999, -0.3305531, 0.0017458396, 0.10818422, -0.21325395, -0.08807993, -0.110342845, 0.10082142, -0.051594347, 0.24192205, -0.18042035, -0.0095462985, -0.08757798) * go_2(0.0, 1.0);\n    result += mat4(0.096379586, 0.021887815, -0.05097233, -0.06797989, -0.026171045, 0.022944937, -0.015915364, 0.037667938, 0.17216732, -0.014889412, 0.07343887, 0.028236505, 0.0015047621, 0.1355103, -0.09918284, -0.07673695) * go_2(1.0, -1.0);\n    result += mat4(-0.25385055, 0.15163356, 0.0030003798, 0.18464413, 0.05611221, 0.099498056, -0.07128191, 0.042955168, 0.027493173, 0.07440157, 0.07814497, 0.096160784, 0.13571084, 0.056412842, -0.031997006, -0.16073681) * go_2(1.0, 0.0);\n    result += mat4(-0.21634746, 0.025153082, -0.064477116, 0.0005679147, -0.0029436245, 0.12794618, 0.024849026, 0.03018052, 0.11723976, 0.059955597, -0.013594654, 0.09091745, 0.04775348, 0.21260159, -0.07463213, -0.06727042) * go_2(1.0, 1.0);\n    result += mat4(-0.12166018, 0.024545137, 0.08611618, -0.17627168, 0.09042604, -0.14157623, -0.22147785, 0.09100581, 0.11078359, 0.031410985, -0.17170976, 0.09532806, -0.059569277, 0.09392676, 0.11784347, -0.21471368) * go_3(-1.0, -1.0);\n    result += mat4(0.1483187, -0.2217563, 0.12032977, 0.14932398, 0.27428308, -0.04568031, 0.12670338, 0.09586169, 0.06700745, 0.005126449, 0.0027694793, -0.033667028, 0.06447861, -0.08585174, -0.05509812, -0.11358761) * go_3(-1.0, 0.0);\n    result += mat4(-0.22750492, 0.032906335, -0.029479047, 0.11580199, -0.05812372, -0.032269973, 0.05219915, 0.041658226, 0.010897959, 0.065550454, 0.0076911976, -0.045743827, 0.11614996, -0.10393113, -0.0012606392, -0.034367524) * go_3(-1.0, 1.0);\n    result += mat4(0.09350742, 0.09561609, 0.3735968, 0.031685118, -0.042026598, 0.17006761, -0.3910107, 0.16984761, 0.25679177, 0.036610503, -0.13772772, 0.11101589, -0.1137049, 0.07211461, 0.18065079, -0.12324793) * go_3(0.0, -1.0);\n    result += mat4(-0.020749722, 0.14413361, -0.061903823, -0.21550268, 0.31306142, -0.11532895, 0.029482557, 0.03282164, -0.09800627, -0.20765196, 0.33030233, 0.075725295, 0.49252015, 0.042455837, -0.07264194, -0.10401895) * go_3(0.0, 0.0);\n    result += mat4(-0.22697076, -0.15738785, 0.09740376, -0.072098814, -0.06638972, 0.12336611, 0.0073687397, 0.048267826, 0.06717852, -0.027047804, -0.123397194, 0.17829034, 0.04215185, 0.066311836, -0.061742183, -0.046373066) * go_3(0.0, 1.0);\n    result += mat4(0.041311592, 0.2813485, 0.055084586, -0.01823069, 0.08105147, -0.087944716, -0.10135052, -0.02653456, 0.063169874, -0.1351186, 0.06722432, -0.016406318, 0.08666922, 0.0555909, 0.12086502, -0.17224412) * go_3(1.0, -1.0);\n    result += mat4(0.26026788, -0.18303715, 0.029279215, -0.12858874, 0.027197823, 0.0919464, 0.00849638, 0.10547888, -0.12952055, -0.14414985, 0.1903315, 0.05004528, -0.12657289, 0.038008716, -0.036606666, -0.054025438) * go_3(1.0, 0.0);\n    result += mat4(0.069167465, 0.2699947, -0.11137602, -0.05888806, -0.107324794, -0.07598601, 0.06042177, 0.0064530694, -0.039780665, -0.076666445, -0.00846108, -0.06165907, -0.06978219, -0.19108103, -0.040026028, -0.120319635) * go_3(1.0, 1.0);\n    result += vec4(-0.14375664, -0.0056876075, 0.052177623, 0.07152566);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!SAVE conv2d_5_tf\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.15667982, -0.31441393, 0.29112124, -0.15737213, 0.022372838, 0.10690639, -0.12019085, -0.051941186, -0.30367845, 0.02612279, 0.2372532, 0.2021648, -0.20481086, -0.003770439, 0.14981231, 0.066780254) * go_0(-1.0, -1.0);\n    result += mat4(0.03270688, -0.42270073, 0.044317324, 0.15907793, 0.14681059, -0.2934784, 0.24933252, -0.067273855, 0.07752533, -0.23194817, 0.0686707, 0.08999225, 0.121678345, -0.12916678, 0.012397381, 0.012315053) * go_0(-1.0, 0.0);\n    result += mat4(-0.10090412, -0.20792678, 0.11076032, -0.02938975, -0.1944187, -0.2003259, 0.04438032, 0.36946484, -0.019868722, -0.15830222, 0.042811528, 0.015641417, 0.113098525, 0.080257006, 0.011135628, -0.2877629) * go_0(-1.0, 1.0);\n    result += mat4(0.15482685, 0.06579119, 0.28301102, 0.23729764, 0.15990537, 0.4529694, 0.107880585, 0.10668121, -0.42430598, -0.2631025, 0.10513542, -0.036242936, -0.09827965, -0.0069260495, -0.11689201, -0.041436482) * go_0(0.0, -1.0);\n    result += mat4(0.08472191, -0.13051608, 0.047930017, 0.36831668, 0.1164478, 0.21384816, 0.22062506, 0.2094167, 0.48668453, 0.32302913, 0.36268055, -0.091801375, -0.079141125, -0.26613805, -0.16608004, 0.03810683) * go_0(0.0, 0.0);\n    result += mat4(-0.13474251, -0.04824603, 0.23303726, -0.116136365, 0.0056330245, 0.15829784, 0.0012259148, 0.12648389, 0.038680512, 0.05131116, 0.024099711, 0.4555406, 0.0035716395, 0.11633299, 0.094744846, -0.2457627) * go_0(0.0, 1.0);\n    result += mat4(-0.0576871, -0.04037522, 0.16857862, 0.0031084458, -0.027274646, -0.18154246, 0.13337846, 0.035422433, -0.0030749738, -0.17288287, 0.019983152, -0.31871706, -0.03280405, 0.06825421, -0.1563798, 0.05031885) * go_0(1.0, -1.0);\n    result += mat4(-0.066631876, 0.012560506, 0.1690693, -0.018248236, 0.0450104, 0.016296914, -0.14910112, -0.16191053, 0.5078224, -0.017615631, 0.15226597, -0.13373777, 0.20148668, 0.060258996, 0.13215344, 0.18430072) * go_0(1.0, 0.0);\n    result += mat4(0.12976126, -0.072738245, 0.053067926, 0.09752956, -0.04716214, 0.04136464, 0.014162617, -0.06621296, -0.09617736, 0.057469178, 0.01280261, -0.042976785, -0.12570308, 0.006027807, 0.031038594, 0.06569918) * go_0(1.0, 1.0);\n    result += mat4(-0.12655424, -0.41563693, -0.030971345, -0.06357555, -0.14121394, -0.15667427, 0.14398985, 0.05995984, 0.0821605, 0.12462943, 0.007492498, -0.0030187522, -0.22804567, -0.10487421, 0.13180672, -0.13978589) * go_1(-1.0, -1.0);\n    result += mat4(-0.075991526, 0.12352044, -0.17844258, 0.010614991, -0.18293494, 0.25009897, -0.080779895, 0.21548378, 0.22215544, 0.048670914, -0.057372037, 0.078176, 0.17490411, 0.004919551, 0.059619516, 0.12660357) * go_1(-1.0, 0.0);\n    result += mat4(-0.06282951, 0.10929357, 0.026720649, -0.15939257, 0.17107709, -0.04334904, -0.03047162, -0.101681694, 0.03118431, 0.19994627, 0.025729552, 0.035035726, -0.0012207883, -0.08618888, 0.061205562, 0.009940555) * go_1(-1.0, 1.0);\n    result += mat4(-0.23581573, 0.08002133, -0.15170844, 0.08872338, -0.25767094, -0.09273545, 0.18153891, 0.2544269, -0.084598936, -0.089766875, -0.14610913, 0.002247754, 0.1802837, -0.019625561, 0.30239686, -0.032793984) * go_1(0.0, -1.0);\n    result += mat4(0.5223286, 0.10347663, 0.4000593, 0.25440502, -0.07646958, -0.31940606, 0.053407036, -0.09356492, 0.2738851, 0.23945184, -0.2907089, -0.45822915, 0.13415676, 0.17187089, 0.08731114, -0.27670014) * go_1(0.0, 0.0);\n    result += mat4(0.059273496, -0.107137166, 0.12087539, 0.179237, -0.021209063, -0.02548005, 0.061256204, 0.033822674, 0.54491127, -0.2475085, 0.08055858, -0.4071213, -0.045093834, 0.07161349, 0.08219979, -0.31735933) * go_1(0.0, 1.0);\n    result += mat4(-0.29527053, 0.021469543, 0.07202354, -0.07103959, 0.03990857, 0.2490762, -0.19419849, -0.13916986, -0.05325315, 0.12922864, -0.041463424, -0.031249814, 0.073991664, -0.09723187, 0.35132217, 0.024760868) * go_1(1.0, -1.0);\n    result += mat4(0.09606787, -0.0951808, -0.0059865676, -0.052033573, -0.3118038, 0.4432636, -0.12943317, 0.09484738, 0.10621756, -0.10550469, 0.11264014, 0.1402276, -0.012679125, -0.08809835, 0.029994955, -0.15121669) * go_1(1.0, 0.0);\n    result += mat4(0.123397775, 0.048338536, -0.00975707, -0.103767075, -0.041053303, -0.07228534, 0.046792876, 0.0668788, 0.29554394, 0.012451002, 0.19568972, 0.112091154, 0.10882395, -0.0995439, 0.051324263, 0.24967718) * go_1(1.0, 1.0);\n    result += mat4(0.2699648, 0.17300771, -0.16056584, 0.1099392, 0.11674778, -0.19811755, 0.111880325, -0.06075038, -0.095849104, -0.04510651, -0.04180761, -0.0052786698, 0.11037549, -0.24115366, 0.018509468, -0.07819484) * go_2(-1.0, -1.0);\n    result += mat4(0.10981622, 0.044488225, 0.050722387, -0.3146652, -0.0013019707, -0.24084032, -0.10475088, 0.026944289, 0.1592903, 0.33087498, 0.061839584, -0.043863457, -0.06904603, -0.08635262, 0.088630445, -0.15485142) * go_2(-1.0, 0.0);\n    result += mat4(-0.06810522, 0.19927117, -0.08130387, 0.11612667, -0.015104349, -7.738651e-05, -0.06419643, -0.14813533, 0.026650215, 0.015038833, 0.08161237, 0.058321163, 0.015005185, -0.16189656, 0.024501886, 0.1927279) * go_2(-1.0, 1.0);\n    result += mat4(0.31858218, 0.11962043, -0.20560326, -0.13190113, 0.02138715, -0.057066392, -0.085771754, -0.124566585, 0.044749223, 0.13687828, 0.1195792, 0.14021616, 0.26204133, 0.05119197, -0.13980037, 0.050747477) * go_2(0.0, -1.0);\n    result += mat4(-0.21238558, -0.0734057, -0.2036023, -0.34308743, -0.29370925, 0.2393742, -0.37877437, 0.036869828, -0.17053255, -0.26900926, -0.23330869, 0.32902205, -0.4882585, 0.27430108, -0.033711653, 0.15501487) * go_2(0.0, 0.0);\n    result += mat4(0.23487025, 0.085289046, -0.14281847, 0.12543266, 0.15871634, -0.13858907, 0.14810285, -0.0239261, 0.1286852, 0.07754033, 0.01072327, -0.14313328, 0.05480442, -0.12195059, 0.11341822, 0.08224607) * go_2(0.0, 1.0);\n    result += mat4(0.19490337, 0.023521842, -0.24548791, 0.0035114093, -0.07937166, -0.07674376, 0.08365873, -0.003286068, 0.023862893, 0.009626835, 0.032829892, 0.0078141205, 0.053484406, -0.08297165, 0.09303188, 0.004273738) * go_2(1.0, -1.0);\n    result += mat4(-0.0032906602, 0.13636959, 0.027821168, 0.06270053, 0.024775786, -0.077529594, 0.03799126, 0.030000908, 0.031749167, 0.04360487, 0.004448846, -0.17835903, -0.30834544, 0.013150946, -0.13758293, -0.03296242) * go_2(1.0, 0.0);\n    result += mat4(-0.14166978, 0.034131095, 0.049779188, 0.09453289, -0.011406557, -0.07020709, -0.0031981543, -0.03443845, -0.00010218944, 0.0855161, -0.10951453, 0.042758763, 0.1718446, -0.1577923, 0.0410027, -0.04992991) * go_2(1.0, 1.0);\n    result += mat4(0.1219178, 0.105126485, -0.041097324, -0.08110963, -0.04857337, -0.11544925, -0.14572923, 0.092435546, 0.091857366, 0.15425235, -0.020324683, -0.05764375, -0.020458939, -0.10527823, -0.085554086, 0.16358297) * go_3(-1.0, -1.0);\n    result += mat4(-0.12372687, -0.009976829, 0.14252265, -0.1321053, -0.05965866, -0.1393898, -0.017603246, -0.02714342, -0.16824952, -0.23083204, -0.012299022, -0.06689838, -0.015830487, 0.21299921, -0.11637202, 0.0074968333) * go_3(-1.0, 0.0);\n    result += mat4(-0.01979935, -0.182785, -0.015397454, 0.14175794, -0.011465284, 0.11285164, -0.036115747, 0.07150463, -0.083641894, -0.10221778, -0.13871445, 0.099696055, 0.04603662, -0.06463785, -0.007984529, -0.0032940735) * go_3(-1.0, 1.0);\n    result += mat4(0.072830334, -0.057334073, 0.09086239, 0.13039105, 0.06350303, 0.17130788, -0.2181585, -0.09137403, -0.31397742, -0.019071499, -0.017274613, 0.13762084, 0.10195637, -0.021455176, 0.04011394, -0.08029658) * go_3(0.0, -1.0);\n    result += mat4(-0.26982597, -0.40265098, -0.4151411, 0.038557775, -0.095602125, 0.3503172, -0.029988842, -0.03484708, 0.095536314, -0.0030311556, 0.31589827, 0.52763534, -0.12629713, -0.24356791, 0.0059487303, 0.42298427) * go_3(0.0, 0.0);\n    result += mat4(0.054166105, 0.18827972, -0.081673265, -0.06720384, 0.09375001, 0.22173035, -0.14050071, 0.108400136, -0.15553835, -0.08716729, -0.037366748, 0.10971073, -0.02560103, -0.26702073, -0.05201882, 0.2432563) * go_3(0.0, 1.0);\n    result += mat4(0.16196893, 0.0889265, -0.09887943, -0.042956755, -0.054403376, -0.123823255, 0.045847844, 0.017027669, 0.00539936, -0.112265736, 0.050549984, -0.104931094, -0.06883012, -0.25745714, 0.11155538, -0.15363649) * go_3(1.0, -1.0);\n    result += mat4(-0.22157209, 0.18200903, -0.13290548, 0.026721261, -0.06066069, -0.18150693, 0.08768983, 0.037362453, -0.1073367, -0.070236765, -0.41223463, -0.168915, -0.15517351, -0.13949952, -0.13307643, -0.15935421) * go_3(1.0, 0.0);\n    result += mat4(-0.026589906, 0.0930502, 0.05195435, 0.06301585, -0.01107014, -0.019382332, 0.027223695, -0.004045145, -0.15238355, -0.0345132, 0.06355168, 0.0011230056, 0.16690113, 0.0017829507, -0.0023939044, -0.09471834) * go_3(1.0, 1.0);\n    result += vec4(0.024455175, 0.01669877, -0.066231176, 0.036848705);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!SAVE conv2d_5_tf1\n//!WIDTH conv2d_4_tf.w\n//!HEIGHT conv2d_4_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.01763509, -0.17156707, -0.06841296, -0.026132878, -0.10600523, 0.11245994, 0.121395074, -0.09331501, 0.12764473, 0.0428028, -0.11837395, 0.2092563, -0.04357652, -0.0490096, 0.024701532, 0.10518723) * go_0(-1.0, -1.0);\n    result += mat4(-0.17130826, -0.31987694, -0.07639005, 0.21362033, 0.058639023, 0.066175915, -0.25344703, -0.07923442, -0.14766373, 0.040518284, -0.031103026, -0.040075514, -0.051108997, -0.28214613, -0.18504949, 0.27544948) * go_0(-1.0, 0.0);\n    result += mat4(0.030991005, -0.011353306, 0.15237464, 0.15458584, 0.1250524, 0.19959912, 0.14049476, 0.38410887, 0.07378578, -0.017728366, 0.0963528, -0.043756213, -0.039577194, -0.11800575, -0.08392266, -0.07599512) * go_0(-1.0, 1.0);\n    result += mat4(0.022089608, -0.027317125, 0.051330008, -0.0075439885, 0.021650828, -0.0009390209, -0.12043464, 0.049332134, -0.055557396, -0.053297505, -0.0918705, -0.13089466, -0.10994107, 0.072746456, 0.11496739, -0.05225977) * go_0(0.0, -1.0);\n    result += mat4(0.29730305, 0.26317745, 0.052159555, -0.32006654, 0.48288685, -0.049926184, -0.08091092, -0.13825637, -0.1485706, -0.288657, -0.41443697, 0.06856032, -0.23809211, -0.12953928, 0.4783034, -0.47557938) * go_0(0.0, 0.0);\n    result += mat4(0.026139118, -0.23031352, 0.04861487, 0.033556074, 0.2702056, 0.22802536, -0.15385233, 0.1664119, 0.18749923, 0.36927548, -0.011473684, -0.11771165, -0.16859052, -0.4513202, 0.12863952, 0.02482837) * go_0(0.0, 1.0);\n    result += mat4(0.0073229345, -0.061915245, 0.06710329, 0.0062416573, -0.00555983, 0.14592186, 0.11201052, -0.123630054, 0.32611257, -0.11279885, -0.059449438, 0.2891043, -0.10519016, 0.040108994, -0.012468261, 0.02083298) * go_0(1.0, -1.0);\n    result += mat4(-0.057483062, 0.08454755, -0.15529329, -0.12572923, 0.2600099, -0.02319978, -0.04037675, 0.11496361, 0.07728194, -0.12908956, -0.025529336, 0.112581626, 0.02971823, 0.11659056, -0.01298622, 0.017061908) * go_0(1.0, 0.0);\n    result += mat4(0.22417091, -0.00222947, 0.04980858, 0.12260437, -0.025507605, 0.042577885, 0.120813504, -0.048522256, -0.038494784, -0.0072195013, -0.23012944, -0.020850847, -0.078296244, -0.014830018, 0.19759563, -0.10000253) * go_0(1.0, 1.0);\n    result += mat4(-0.032090195, 0.023757193, -0.08989734, 0.14419042, 0.0112194475, -0.093776144, -0.020197887, 0.29295877, 0.06872183, 0.09511462, -0.03245769, -0.06504889, 0.05132126, 0.00399527, 0.075911656, 0.250893) * go_1(-1.0, -1.0);\n    result += mat4(-0.3418496, 0.25525784, 0.0018161442, 0.028484365, -0.17573346, -0.12457501, 0.18466166, 0.20209278, 0.10282706, 0.16353399, 0.025052028, -0.059714165, -0.055806916, -0.28651386, 0.112798095, 0.11624314) * go_1(-1.0, 0.0);\n    result += mat4(-0.018793896, 0.07500149, -0.01728254, -0.1726998, -0.13333, 0.09590344, -0.036537904, -0.11522523, 0.19445558, 0.22680458, 0.12061006, -0.06225618, 0.1127748, 0.28380096, -0.07099846, -0.007440302) * go_1(-1.0, 1.0);\n    result += mat4(-0.43887648, -0.10018577, -0.29267642, 0.12149727, -0.14333835, 0.04161915, 0.19442867, 0.16506511, 0.09655387, -0.0014398015, 0.13189743, -0.14068556, 0.049408, 0.0829072, 0.2950336, 0.36965907) * go_1(0.0, -1.0);\n    result += mat4(0.41486958, -0.023498302, -0.37900022, -0.31752598, 0.13758768, -0.18782206, -0.31358528, 0.3330786, -0.4039293, -0.06539036, 0.032599606, 0.10663507, -0.26369813, -0.17365438, 0.20723309, 0.1801556) * go_1(0.0, 0.0);\n    result += mat4(0.004117444, -0.14894462, 0.14915143, -0.047375835, -0.2609916, -0.10172324, -0.14925237, -0.33830285, 0.12131607, -0.18156646, -0.42382464, -0.052582145, 0.2329045, -0.4576963, 0.13756892, 0.055571318) * go_1(0.0, 1.0);\n    result += mat4(-0.31689477, 0.017058033, -0.01904924, -0.016893756, -0.011479519, 0.07316262, -0.07086077, 0.08923511, -0.08190091, -0.025866933, -0.06909204, -0.028601022, 0.023224542, 0.03082087, 0.2230426, -0.16713654) * go_1(1.0, -1.0);\n    result += mat4(0.13457374, 0.110913865, -0.1130815, -0.031438913, -0.55201167, 0.04831016, 0.25107765, -0.014003224, 0.19532952, 0.02062346, 0.04839241, 0.088673405, 0.30325848, -0.20222804, -0.085780576, 0.22512968) * go_1(1.0, 0.0);\n    result += mat4(0.076354, 0.021940092, -0.16170324, 0.0025543426, -0.0032400405, -0.0046705627, 0.06241069, -0.031247333, 0.098353796, 0.03723474, 0.22971998, -0.017877292, 0.119858086, 0.008041448, 0.2140585, 0.10343376) * go_1(1.0, 1.0);\n    result += mat4(0.08627595, 0.04532834, 0.027579082, -0.16222088, 0.15583228, -0.14371829, -0.07243855, -0.111895435, -0.14438897, -0.10250594, 0.0034202964, -0.066547595, -0.034390844, -0.021545287, 0.014540157, -0.10215731) * go_2(-1.0, -1.0);\n    result += mat4(0.19720152, 0.21534947, 0.1130938, -0.011730973, 0.013247983, -0.10344174, -0.1906514, -0.015767017, -0.020093633, -0.26487067, -0.005960781, -0.057149183, 0.030110173, 0.047692046, -0.19308545, -0.25292158) * go_2(-1.0, 0.0);\n    result += mat4(0.039498243, 0.053682897, -0.01844695, -0.017540915, 0.039454967, -0.27696076, 0.09503274, -0.038958035, 0.17321438, -0.036311295, 0.03123055, 0.02310311, 0.040591653, 0.0054627894, -0.03520426, -0.026101988) * go_2(-1.0, 1.0);\n    result += mat4(0.055991564, 0.06512919, -0.12532505, 0.024075158, -0.04926237, -0.11701171, 0.026792146, 0.013033238, -0.052847516, -0.01550091, -0.008442071, -0.077945165, -0.033220004, -0.13678443, -0.07040586, 0.121846326) * go_2(0.0, -1.0);\n    result += mat4(-0.19537796, -0.016634773, 0.10707109, -0.024361614, -0.16002733, -0.44066608, 0.16488662, 0.013152995, 0.22407806, 0.12854017, 0.19028598, -0.08379244, -0.05594235, -0.15909895, 0.511962, 0.39027596) * go_2(0.0, 0.0);\n    result += mat4(-0.032652248, 0.06004893, 0.011166194, 0.102761306, -0.035113614, -0.29961765, -0.013817978, 0.20938557, 0.08488225, -0.1118558, -0.0375328, -0.035511103, 0.0046933405, 0.20203683, -0.13552529, -0.12685429) * go_2(0.0, 1.0);\n    result += mat4(0.03054923, 0.08224908, -0.059128158, -0.02583655, -0.02133876, 0.0048713544, 0.10848829, 0.06324404, 0.028332822, -0.011002306, -0.027557913, -0.06072362, 0.1019048, -0.02587316, 0.08563405, -0.08119947) * go_2(1.0, -1.0);\n    result += mat4(-0.10568117, 0.1075248, 0.19379964, -0.14337265, 0.019374132, -0.0907804, -0.13827625, -0.03628561, 0.014735499, -0.026882607, -0.25948793, 0.034926686, -0.05988073, -0.22735636, 0.053511668, 0.04765336) * go_2(1.0, 0.0);\n    result += mat4(-0.029848114, 0.09183966, 0.084713496, 0.09422864, 0.069713995, -0.10584984, -0.020899031, 0.059645247, -0.075805016, -0.01828552, 0.06689195, -0.13804196, -0.023465823, -0.034038994, -0.12946706, 0.058709413) * go_2(1.0, 1.0);\n    result += mat4(0.061918218, 0.038984764, 0.013660938, -0.19340219, -0.014949839, 0.12946278, 0.12725051, 0.13429146, 0.05993008, -0.015394284, 0.011232483, 0.0344157, 0.022161875, -0.023923954, 0.061736204, 0.025963215) * go_3(-1.0, -1.0);\n    result += mat4(0.048136763, 0.03162042, -0.01967249, 0.06374493, 0.034645267, 0.22403605, 0.036197048, -0.06903216, -0.1024706, -0.0005459356, 0.049185563, 0.16309108, 0.07394778, 0.10351343, 0.28430694, -0.13531347) * go_3(-1.0, 0.0);\n    result += mat4(-0.14705071, -0.09458433, 0.03063114, 0.07901115, -0.11911086, -0.06428132, -0.013549552, -0.041342866, -0.20770676, -0.15104479, 0.054365363, -0.11652907, 0.05639815, 0.070518605, 0.0017846811, -0.00056205114) * go_3(-1.0, 1.0);\n    result += mat4(0.27148908, 0.07358356, 0.13644488, -0.13824654, 0.0112991175, -0.021521023, -0.10197379, 0.007816017, -0.13314332, 0.12318473, -0.043214846, -0.15759036, -0.19744353, -0.10267182, -0.28249928, 0.11233295) * go_3(0.0, -1.0);\n    result += mat4(-0.096474804, 0.17893109, 0.014679829, -0.21218887, -0.24170275, 0.10603527, 0.05375366, -0.059315052, 0.17087384, 0.13633691, -0.37958893, 0.43264794, 0.17829923, 0.06485103, -0.37551817, -0.22082718) * go_3(0.0, 0.0);\n    result += mat4(-0.30536333, -0.033212308, -0.25232, 0.11730442, -0.11176368, 0.26223183, -0.049025323, -0.01375941, -0.29028055, 0.16842811, -0.035684332, -0.4180911, -0.1611732, 0.07683385, -0.14263596, 0.17508087) * go_3(0.0, 1.0);\n    result += mat4(0.23580009, 0.025621435, -0.15757325, 0.008123166, -0.021905439, -0.02162503, -0.059497356, -0.01636353, 0.047654126, -0.084423855, -0.033733923, 0.0127116265, -0.059593942, -0.053935718, -0.050729543, 0.013887048) * go_3(1.0, -1.0);\n    result += mat4(-0.19232626, 0.07915767, -0.05909752, 0.007695347, 0.058876406, 0.057521783, -0.080253534, 0.2011056, -0.27965516, -0.08033169, -0.13025513, 0.12854645, 0.053400308, -0.18445957, -0.18463044, 0.27920377) * go_3(1.0, 0.0);\n    result += mat4(-0.061806213, -0.020037206, 0.003183183, -0.029844081, -0.039553937, 0.028905323, -0.11367984, -0.097321615, -0.10112643, 0.0039709485, -0.06020118, -0.23871279, -0.077974856, 0.05806996, -0.21440302, 0.11898043) * go_3(1.0, 1.0);\n    result += vec4(-0.023832673, 0.03702965, -0.04749135, -0.10982549);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!SAVE conv2d_6_tf\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.030931145, 0.013683292, -0.0650242, -0.028732346, 0.120067924, -0.029404473, 0.0038229884, -0.14631765, 0.041900825, -0.076596744, -0.11096378, -0.27100095, 0.0052598766, -0.05929686, -0.06816563, -0.086864315) * go_0(-1.0, -1.0);\n    result += mat4(-0.043620087, -0.16360405, 0.006527374, 0.15706524, 0.08338088, -0.19027525, 0.22595987, -0.054963548, 0.01825031, -0.03149212, 0.025471251, 0.06429379, -0.011633275, -0.079389006, -0.0030728737, 0.17345747) * go_0(-1.0, 0.0);\n    result += mat4(-0.011275288, -0.10668036, 0.05718997, 0.010336089, 0.33393976, -0.2029354, 0.075444475, -0.092244044, 0.07605498, 0.20125951, 0.10493973, -0.12306946, 0.03658231, 0.08233366, -0.12205888, -0.116969004) * go_0(-1.0, 1.0);\n    result += mat4(-0.0070305974, 0.105127215, 0.006041873, 0.26743913, 0.028119443, 0.14823505, -0.28344348, 0.12362866, -0.1215781, 0.08104382, 0.102011785, 0.085380934, 0.061244503, -0.06230063, -0.05353345, 0.1166729) * go_0(0.0, -1.0);\n    result += mat4(0.08945733, 0.4101902, -0.06404005, 0.040728435, 0.13076581, -0.20805469, -0.10897316, -0.14924604, 0.10090762, 0.015475414, 0.26346552, 0.12096677, -0.20199244, 0.2780031, 0.18515368, 0.35105625) * go_0(0.0, 0.0);\n    result += mat4(0.07463155, 0.26932517, -0.06768551, 0.10470878, -0.1423996, 0.013550665, -0.06167201, -0.1022994, -0.3107166, -0.15609552, 0.1695213, -0.1277181, 0.12582655, -0.1596128, 0.015612055, -0.19826376) * go_0(0.0, 1.0);\n    result += mat4(0.011745468, 0.006471601, 0.008110513, 0.025831396, 0.1272883, -0.221959, 0.11993834, -0.007903633, 0.009993582, -0.10170755, 0.026594637, -0.027883623, 0.030666083, -0.036415886, 0.007469573, 0.0674783) * go_0(1.0, -1.0);\n    result += mat4(-0.022760388, -0.10911659, -0.012589904, -0.046462692, 0.36987287, 0.71668935, -0.04466556, 0.12082762, 0.0026539841, 0.07070946, -0.00020439121, -0.13925348, 0.08672072, 0.20075354, -0.066352285, 0.14655356) * go_0(1.0, 0.0);\n    result += mat4(-0.081081845, -0.21956222, 0.06781787, -0.106362104, -0.03016425, -0.010460211, -0.009725996, -0.009805538, 0.07037355, 0.19254607, 0.038890257, 0.29580075, -0.10355764, 0.12613009, 0.02485986, -0.031927988) * go_0(1.0, 1.0);\n    result += mat4(-0.13882205, 0.21770848, 0.015392157, 0.010310204, 0.008225721, 0.07457836, 0.09984027, -0.25452816, 0.2193511, -0.22262146, -0.12950355, 0.026151875, 0.022114651, -0.030566849, 0.034688126, 0.03047327) * go_1(-1.0, -1.0);\n    result += mat4(0.0363441, 0.19290726, -0.1143055, 0.30871987, -0.05780708, 0.082128406, -0.115280904, 0.07636388, 0.48947453, -0.29715258, 0.146737, -0.3275992, -0.055972476, -0.09991753, 0.17435446, 0.10917291) * go_1(-1.0, 0.0);\n    result += mat4(0.026389305, 0.054523308, -0.028950177, 0.06913328, -0.18626037, 0.08829993, 0.10407121, 0.001246911, 0.103938825, -0.3117343, -0.045564886, 0.07316613, 0.0027089121, 0.099437356, -0.046500806, -0.0927284) * go_1(-1.0, 1.0);\n    result += mat4(0.051037624, -0.2068234, 0.061572235, -0.3345198, 0.16960172, -0.30289862, -0.002583443, 0.39312238, 0.08246557, 0.16374862, -0.31902805, -0.13205275, -0.032050006, 0.01670186, 0.13852347, 0.120012194) * go_1(0.0, -1.0);\n    result += mat4(-0.67096996, -0.06274476, 0.18575665, 0.80282855, 0.23201196, -0.0054729837, 0.050396994, -0.42014772, 0.34904522, 0.26281372, 0.24697208, 0.55475426, 0.49850988, -0.06581312, -0.0068906257, -0.15741143) * go_1(0.0, 0.0);\n    result += mat4(-0.04252036, -0.28224963, 0.009723064, 0.116357096, 0.2992567, -0.26702902, -0.05648925, 0.12729199, -0.37574205, 0.54211813, -0.25248805, -0.13023548, 0.18903324, -0.5182459, 0.0141203115, -0.19444294) * go_1(0.0, 1.0);\n    result += mat4(-0.0017735233, -0.010132458, -0.040924776, -0.13767008, 0.20757031, -0.06509882, -0.09756446, 0.018974079, 0.090851985, -0.010158765, -0.03999607, -0.12055641, 0.03629025, -0.018645551, -0.05506811, -0.014202848) * go_1(1.0, -1.0);\n    result += mat4(0.16203491, 0.011118734, -0.18486023, -0.024290733, -0.3673846, -0.20295864, 0.23055002, -0.1555852, -0.02706522, 0.03262891, 0.008724611, -0.03760652, -0.20946771, -0.01951837, 0.16955496, 0.11690098) * go_1(1.0, 0.0);\n    result += mat4(0.0783421, 0.22656651, -0.15715368, -0.024174158, 0.020260733, 0.032390315, -0.029133298, 0.086601086, 0.13871798, -0.12525433, 0.16097449, 0.058946393, 0.029865682, 0.08508385, 0.040569812, -0.09402932) * go_1(1.0, 1.0);\n    result += mat4(-0.05063873, 0.11269313, -0.057484943, -0.13579641, 0.047973365, -0.07103839, -0.07838756, -0.0028928046, -0.019466015, 0.018428024, 0.010016324, -0.057396665, -0.19495595, 0.034307264, -0.022888038, 0.08112259) * go_2(-1.0, -1.0);\n    result += mat4(-0.09790086, 0.10613111, 0.06611674, 0.19356097, -0.00073371036, -0.019078335, 0.076719105, -0.016212497, -0.3283475, -0.07547389, -0.08140701, 0.3185625, -0.25060275, 0.16820994, -0.123497784, 0.43272668) * go_2(-1.0, 0.0);\n    result += mat4(-0.06365342, 0.11186735, -0.17493224, -0.04207358, 0.0003117533, 0.034089327, -3.067692e-05, -0.03422754, 0.16267666, 0.054771993, 0.048384454, -0.041866794, 0.0036008756, 0.0021496525, 0.20258942, -0.06297619) * go_2(-1.0, 1.0);\n    result += mat4(0.03578836, 0.08763908, -0.22370125, -0.32465744, 0.019142643, 0.011316954, 0.17920344, 0.031633645, 0.03766343, -0.116487674, -0.05281752, -0.018965483, 0.049297336, -0.34511214, 0.42598158, 0.051361635) * go_2(0.0, -1.0);\n    result += mat4(0.26638633, -0.33628765, 0.04437907, 0.09616201, -0.020049393, 0.2560829, -0.027108455, 0.255752, 0.3666511, 0.052277412, -0.46667686, 0.48482272, 0.51302284, -0.06941614, -0.17967525, -0.07889891) * go_2(0.0, 0.0);\n    result += mat4(0.18503937, 0.088710256, 0.2083147, -0.20758459, -0.036416974, 0.018303726, 0.03729963, -0.035969947, -0.2685231, -0.42169708, -0.039593916, -0.02642618, 0.29050872, -0.25723743, -0.111259766, 0.15001127) * go_2(0.0, 1.0);\n    result += mat4(-0.026473878, -0.07241443, 0.022400148, -0.03214132, 0.0859297, -0.0036677981, -0.07039137, 0.03703108, 0.042322673, -0.01222808, -0.08151938, 0.033109214, -0.048737407, 0.25929528, -0.40535828, -0.123594694) * go_2(1.0, -1.0);\n    result += mat4(0.10233285, 0.22455986, -0.13368733, 0.033236265, -0.052114893, -0.11709317, 0.009709581, 0.19201641, -0.02973698, 0.032114245, -0.09771862, 0.085680574, 0.15827927, -0.15042172, 0.21833214, -0.13262676) * go_2(1.0, 0.0);\n    result += mat4(-0.08460587, -0.09473209, 0.019323658, -0.057233352, 0.0019434267, -0.14437936, 0.034232683, 0.0030602294, -0.023598112, 0.10692026, -0.09960999, 0.005887181, 0.014738836, -0.32473162, -0.10886747, -0.08365826) * go_2(1.0, 1.0);\n    result += mat4(0.10900178, 0.00080280803, -0.14009437, -0.053074867, -0.07811151, -0.03456029, -0.104943685, 0.016918905, -0.11335709, 0.079421654, 0.13481963, 0.037818357, -0.027339859, 0.05856774, -0.044562265, 0.03908084) * go_3(-1.0, -1.0);\n    result += mat4(0.07628258, -0.23815769, 0.2840278, -0.3541637, -0.044292126, -0.09310441, -0.1335055, -0.031899665, -0.11981227, 0.24012394, -0.041896038, -0.10168982, 0.20248915, -0.10036763, -0.044115108, 0.08520525) * go_3(-1.0, 0.0);\n    result += mat4(0.07234102, -0.119480744, -0.01401321, -0.025182616, -0.031284854, -0.050089385, 0.014808948, 0.038662236, -0.18539418, 0.017342187, 0.023812262, 0.13428104, 0.020824855, -0.07433546, 0.054307282, 0.08511016) * go_3(-1.0, 1.0);\n    result += mat4(-0.11046813, -0.04663274, 0.33497185, 0.023273284, -0.24681108, 0.116665915, 0.12045893, 0.13306482, -0.039098527, 0.04747061, 0.042796664, 0.053514794, 0.011861975, -0.048702, 0.008408589, -0.09497112) * go_3(0.0, -1.0);\n    result += mat4(0.34634927, 0.37973458, -0.79267627, -0.7362719, 0.35489878, -0.07635863, 0.24082923, -0.27480397, -0.3236968, -0.25523046, 0.05118527, -0.040529836, -0.6000509, 0.39020586, 0.27632973, 0.5141453) * go_3(0.0, 0.0);\n    result += mat4(0.16761221, -0.033125393, 0.00561569, 0.083019435, -0.101278506, 0.07810264, 0.12060661, 0.16048536, 0.14257826, -0.15996903, 0.018831912, -0.094429865, -0.22227801, 0.426937, -0.054677445, 0.05067348) * go_3(0.0, 1.0);\n    result += mat4(0.02233958, 0.02608942, -0.045318656, 0.06509929, 0.035911568, 0.025316885, 0.0840986, 0.08326237, 0.048455603, -0.13630742, 0.07230253, -0.047261715, -0.092630014, 0.04786565, 0.10354939, -0.07094341) * go_3(1.0, -1.0);\n    result += mat4(-0.1463382, -0.14900577, 0.2835977, -0.106733374, -0.11554754, -0.168429, -0.1411373, -0.20654152, -0.06388508, 0.039648015, 0.08543832, -0.13253337, 0.017264463, -0.06346233, -0.10823598, 0.067361064) * go_3(1.0, 0.0);\n    result += mat4(0.04419582, 0.039152585, 0.06222691, 0.05757103, 0.012084537, 0.051425997, -0.061130576, 0.16752882, 0.07497411, 0.13495837, -0.15585983, -0.02050144, -0.08555421, -0.09147339, 0.025115604, 0.05948922) * go_3(1.0, 1.0);\n    result += vec4(0.00590038, 0.03082865, 0.002111702, -0.03330112);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16\n//!HOOK MAIN\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!SAVE conv2d_6_tf1\n//!WIDTH conv2d_5_tf.w\n//!HEIGHT conv2d_5_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\n#define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0))\n#define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.009029573, 0.029218858, 0.029705316, -0.019268971, -0.0023235187, -0.072589695, 0.1424836, 0.09049359, 0.04342995, 0.18134294, 0.018145641, 0.14789368, 0.050923645, 0.06524081, 0.036812488, 0.11108108) * go_0(-1.0, -1.0);\n    result += mat4(-0.026506428, 0.016968496, 0.015961196, 0.010030791, -0.3141888, -0.06769598, -0.23920257, -0.031002127, -0.07351358, -0.19290134, -0.24282931, -0.18831016, -0.0928966, 0.075177215, -0.19699521, -0.05810917) * go_0(-1.0, 0.0);\n    result += mat4(-0.017991852, -0.079427645, 0.035970494, -0.017095685, -0.27197137, -0.20046075, 0.2616644, 0.021876303, -0.077394076, -0.04978692, 0.20363241, -0.013741705, -0.032103598, 0.14403099, 0.01442474, 0.048115995) * go_0(-1.0, 1.0);\n    result += mat4(-0.16939245, -0.001777, 0.026244136, -0.14122388, -0.056853324, 0.54357284, -0.19769607, -0.03187079, 0.04559263, -0.16048127, 0.12830622, 0.1442168, 0.006611398, -0.01618195, 0.012860053, -0.16539487) * go_0(0.0, -1.0);\n    result += mat4(0.13116026, -0.006161343, 0.7209969, 0.18338475, 0.3099777, 0.6500026, 0.3883795, -0.021434233, 0.31667513, 0.008917659, 0.14124091, -0.22335114, 0.12198921, -0.16449445, 0.08773425, 0.30054978) * go_0(0.0, 0.0);\n    result += mat4(-0.10413989, -0.10316161, 0.04342709, -0.021252686, 0.120892406, 0.37798002, -0.35963747, 0.021069285, 0.37587845, -0.08159587, 0.011139747, 0.2501104, -0.094568014, 0.037900843, -0.025109999, -0.030106556) * go_0(0.0, 1.0);\n    result += mat4(0.09680291, -0.040868275, 0.051731605, 0.089064725, -0.56098557, -0.38148618, -0.017037416, 0.08508287, -0.019247344, 0.019857002, -0.03512887, 0.031057188, -0.09648583, -0.04474188, 0.028748507, -0.11880965) * go_0(1.0, -1.0);\n    result += mat4(-0.010236943, 0.04257042, -0.08202597, -0.004203426, -0.26801527, -0.11716526, -0.017402772, -0.05819106, -0.13394608, 0.0234606, -0.15404865, -0.06801164, -0.0047627664, -0.1975249, 0.09420144, 0.23249897) * go_0(1.0, 0.0);\n    result += mat4(0.107361935, 0.07373787, 0.06242962, 0.05236332, -0.028867323, 0.025924044, -0.042526353, -0.0015729597, -0.1323144, -0.4040712, 0.023919407, -0.09535502, 0.049100045, 0.081110805, 0.08946112, 0.058505684) * go_0(1.0, 1.0);\n    result += mat4(0.13236825, -0.04468476, -0.04426802, 0.031087106, -0.09093992, -0.07470971, -0.01591504, 0.05924266, -0.21910913, 0.065537, -0.18358919, -0.02533145, -0.1512009, -0.04953928, 0.015540006, -0.0043442883) * go_1(-1.0, -1.0);\n    result += mat4(-0.14016777, -0.1086958, 0.16316028, 0.050777458, 0.23148167, 0.04944809, -0.10599886, -0.10447021, -0.40729257, -0.10926556, 0.069055155, 0.110635415, 0.108922414, -0.1716362, 0.10743909, -0.102534756) * go_1(-1.0, 0.0);\n    result += mat4(0.017795928, -0.066930935, 0.09396082, 0.092585504, 0.14223933, 0.059458215, 0.072033696, -0.04507726, -0.19956456, 0.1251282, -0.31733638, -0.10465904, 0.08546377, 0.048638333, 0.031372465, -0.08720661) * go_1(-1.0, 1.0);\n    result += mat4(0.108719654, -0.092161916, -0.014724377, 0.20068261, -0.24350016, 0.2113636, -0.07483714, -0.45665312, -0.25134233, 0.2753893, -0.11324696, -0.04472, 0.1576102, -0.045395147, 0.06013951, -0.12507361) * go_1(0.0, -1.0);\n    result += mat4(0.546225, -0.281897, 0.19477816, -0.116612464, -0.3145171, -0.41660902, 0.333625, 0.35902345, 0.48333502, 0.4662005, 0.10222491, -0.15314859, -0.3036888, 0.22849742, 0.20740797, 0.41399437) * go_1(0.0, 0.0);\n    result += mat4(0.007284074, 0.0393942, -0.31192186, -0.15687793, -0.289214, -0.015956698, -0.24718472, -0.1637855, -0.00765037, 0.26677555, 0.20215511, 0.37790874, -0.22096673, 0.25287116, -0.2446764, -0.13610223) * go_1(0.0, 1.0);\n    result += mat4(-0.16734968, 0.16721225, -0.053508647, -0.041097626, 0.062356673, 0.07812319, -0.263546, -0.39739034, 0.003389846, 0.12676363, -0.13175991, -0.19019242, -0.011847587, -0.007580052, -0.023946386, 0.046034034) * go_1(1.0, -1.0);\n    result += mat4(-0.17047611, 0.13298693, -0.07506747, -0.045542978, 0.33571973, 0.20192616, 0.30674616, 0.25668672, -0.24134545, 0.031693842, -0.009647641, 0.040534843, 0.03159419, -0.1100516, 0.11371316, 0.06098735) * go_1(1.0, 0.0);\n    result += mat4(-0.05518961, 0.19402988, -0.09646874, -0.059196774, -0.0073436056, -0.1381309, 0.06868669, 0.061328378, -0.1480867, -0.15774113, -0.022572191, 0.122521356, -0.04067007, -0.10145177, 0.13006335, -0.099452734) * go_1(1.0, 1.0);\n    result += mat4(0.06962972, 0.07768411, 0.021085173, 0.108355984, -0.03132525, 0.10220273, -0.11626593, -0.14104277, 0.018778645, -0.024237925, 0.048783034, 0.09074447, 0.4120426, -0.01948466, 0.073218934, 0.055681944) * go_2(-1.0, -1.0);\n    result += mat4(-0.22553118, -0.12923603, -0.22068842, -0.35037905, 0.005709937, -0.09528472, 0.08718399, 0.13200706, 0.17220478, 0.096844435, -0.30439013, -0.14122063, 0.15733318, -0.1014675, 0.33836862, 0.042193163) * go_2(-1.0, 0.0);\n    result += mat4(0.15826897, -0.034870047, 0.09295099, -0.17674965, -0.042326324, 0.06680338, -0.074267656, -0.0631393, -0.11267909, -0.19795708, 0.22005288, 0.35703793, 0.033995766, -0.12663686, -0.02449896, -0.123250045) * go_2(-1.0, 1.0);\n    result += mat4(0.021434195, 0.058398597, 0.04828315, -0.0016824572, -0.04291545, -0.0744907, -0.07698706, -0.15937585, -0.18852457, -0.17966963, 0.023800725, 0.025979731, -0.51412296, -0.018316887, -0.23076254, -0.12298674) * go_2(0.0, -1.0);\n    result += mat4(0.16054317, -0.0002730893, -0.54173076, -0.62443435, 0.04300197, -0.08529622, 0.15392275, 0.15742144, 0.025834514, -0.2800517, -0.17600477, 0.0020806703, -0.3010582, 0.45233512, 0.25595665, 0.103661336) * go_2(0.0, 0.0);\n    result += mat4(-0.024034392, -0.43800178, 0.28606912, -0.20908915, 0.078471914, -0.030501373, -0.059055753, 0.050494444, 0.063274644, -0.025071034, 0.17561312, -0.100698635, -0.25631955, 0.039981876, -0.18506624, 0.08366402) * go_2(0.0, 1.0);\n    result += mat4(-0.1413656, 0.03589635, -0.020917566, 0.017598262, 0.020156413, -0.018854238, 0.027228508, -0.03806087, -0.021715842, 0.071974196, -0.040065665, 0.08459291, -0.23530225, 0.16599682, -0.2772327, 0.10041177) * go_2(1.0, -1.0);\n    result += mat4(-0.055056706, 0.1286236, -0.11890451, -0.1790546, 0.16517544, -0.040448934, 0.12548013, 0.017075695, 0.07185459, -0.13236302, 0.19354409, 0.12767012, 0.31120765, 0.16378082, -0.036915366, -0.19724306) * go_2(1.0, 0.0);\n    result += mat4(-0.02225051, 0.033263147, 0.003279449, 0.08826271, -0.047833472, 6.574577e-05, 0.13721916, 0.04801998, -0.014958419, 0.08791209, -0.08076282, 0.024002168, -0.18028922, 0.23835851, -0.23309888, -0.119310364) * go_2(1.0, 1.0);\n    result += mat4(0.044960875, 0.18821983, 0.027640678, 0.013462449, 0.19011214, 0.21559924, -0.03329638, 0.07234414, 0.030880248, -0.11273214, 0.102028474, 0.12203351, 0.035855662, 0.008828778, 0.007218363, -0.012421797) * go_3(-1.0, -1.0);\n    result += mat4(-0.09450626, 0.025191775, -0.10738468, 0.16237053, 0.073676676, 0.12488881, -0.048748355, 0.007877263, 0.3572506, -0.07911043, 0.14684045, 0.0015310893, -0.33411503, -0.1151223, 0.004201752, 0.017775744) * go_3(-1.0, 0.0);\n    result += mat4(-0.10607509, -0.008143826, -0.08448629, -0.27557802, 0.0046665915, 0.008158659, 0.030826218, 0.020516023, 0.2333065, -0.017463414, -0.041772116, -0.03027809, -0.028166672, -0.080471426, 0.048199337, 0.08341059) * go_3(-1.0, 1.0);\n    result += mat4(-0.14640257, -0.18334304, -0.061674733, 0.0008892598, -0.2374775, -0.2721524, -0.040371176, 0.26362613, 0.19872928, -0.11246391, 0.0842288, 0.11188515, 0.0045209546, -0.04250933, -0.0738212, -0.069005966) * go_3(0.0, -1.0);\n    result += mat4(-0.08760266, 0.4816288, -0.21241407, 0.22734411, -0.1783721, -0.26842996, 0.099888, -0.2867675, 0.085521065, -0.3780281, -0.018543908, -0.039699722, 0.75688565, -0.5333645, 0.47567275, 0.09518891) * go_3(0.0, 0.0);\n    result += mat4(-0.04072665, 0.05998423, -0.48314768, -0.29495844, 0.10358383, -0.09816629, 0.028586809, -0.047708735, 0.008320228, 0.04089551, -0.18359782, -0.27615002, 0.12414414, -0.072417594, 0.25932562, 0.30268723) * go_3(0.0, 1.0);\n    result += mat4(0.14481631, 0.06484443, -0.09898657, -0.06553556, 0.25750044, -0.07265585, 0.12903488, -0.022347894, -0.04693863, -0.000107379274, 0.030295763, -0.0325354, 0.086214684, -0.021326948, 0.039682828, -0.034843277) * go_3(1.0, -1.0);\n    result += mat4(-0.031971477, -0.25145087, 0.03931631, 0.14262606, -0.06044626, 0.22820354, -0.10506207, 0.18064679, 0.0069641788, 0.01477993, -0.003626875, 0.118767865, 0.109416224, -0.002998205, 0.035680585, 0.07843882) * go_3(1.0, 0.0);\n    result += mat4(0.03375426, -0.059815384, 0.11632834, -0.12411481, 0.022583738, 0.02544465, -0.054889992, -0.07031964, -0.10140042, 0.16750422, -0.1448294, -0.09316004, 0.035582513, -0.026138382, -0.031955894, 0.040148776) * go_3(1.0, 1.0);\n    result += vec4(-0.03573331, 0.032919675, 0.011109369, 0.008329268);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!SAVE conv2d_last_tf\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\n#define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(-0.11498094, -0.053904895, -0.11520678, -0.05479549, 0.028396055, 0.032767884, 0.052479446, 0.05257866, -0.25706592, -0.3454966, -0.24713765, -0.2854201, -0.10287636, 0.0023146886, -0.09190338, -0.011193905) * g_0;\n    result += mat4(-0.05461422, 0.008780496, -0.07738697, -0.032230727, -0.047554165, -0.025061952, -0.051897213, -0.009545297, -0.14548294, -0.15184018, -0.01313442, -0.015299784, -0.0007883845, -0.12866738, -0.15260352, -0.27081275) * g_1;\n    result += mat4(0.11007706, 0.035344437, 0.11020841, 0.0425353, 0.1613199, 0.18417408, 0.09274313, 0.11943135, 0.106862, 0.079875536, 0.0937752, 0.068030775, 0.029093558, -0.06441164, 0.06467169, -0.021989612) * g_2;\n    result += mat4(0.049548414, -0.012455486, 0.07185561, 0.021865537, 0.020969186, -0.03374196, -0.024260623, -0.07739141, 0.07164591, 0.12741035, 0.0379913, 0.076403245, 0.07049977, 0.0744538, 0.0062989634, 0.01818882) * g_3;\n    result += mat4(-0.12511204, -0.010836819, 0.13709816, 0.22472954, 0.21280868, -0.006484726, 0.17554289, -0.009977173, 0.078398876, 0.20698707, 0.13432744, 0.29740283, -0.24750128, -0.32757792, -0.19807857, -0.2537023) * g_4;\n    result += mat4(-0.27207088, -0.1385644, -0.2166476, -0.07687419, -0.20300622, -0.29678395, -0.13135734, -0.20851587, 0.0361364, 0.011243289, -0.06845459, -0.11796941, 0.11575868, 0.070215136, -0.10295678, -0.12281369) * g_5;\n    result += mat4(0.13619795, -0.0019436983, -0.12701888, -0.25933513, -0.20134166, 0.00062823144, -0.076756015, 0.11002947, 0.0059049693, -0.18756741, -0.0718802, -0.2589954, 0.23413423, 0.30107784, 0.14445266, 0.18920745) * g_6;\n    result += mat4(0.1494216, 0.0587532, 0.05478662, -0.039123338, 0.23322394, 0.29950607, 0.24384268, 0.27843767, -0.16094431, -0.04705998, -0.016345032, 0.028868208, -0.102872886, -0.04659664, 0.104105346, 0.14305067) * g_7;\n    result += mat4(-0.001037014, 0.010001526, -0.0052278573, 0.024779709, 0.06857274, 0.067640975, 0.085439384, 0.09242789, -0.066597246, -0.055928994, 0.0015658981, 0.016131008, -0.03524695, -0.018364554, -0.047754433, -0.014295886) * g_8;\n    result += mat4(-0.042207, 0.02835915, -0.1404656, -0.08563323, -0.030979915, -0.0673764, 0.10733943, 0.057902794, 0.00022424995, -0.0023634837, -0.10778953, -0.10202357, -0.020368295, -0.019088887, -0.06875738, -0.08504131) * g_9;\n    result += mat4(-0.00043458896, 0.00045652856, -0.02016843, -0.020062413, -0.08740103, -0.042085808, -0.10644177, -0.09226477, 0.11212161, -0.00048174805, 0.021872435, -0.05868698, 0.0333954, 0.058184672, 0.05532576, 0.07621587) * g_10;\n    result += mat4(0.054245148, 0.001020329, 0.09106849, 0.05303779, 0.009889632, 0.01309413, -0.09187347, -0.08618193, -0.011621187, 0.016222361, 0.061095525, 0.060885344, 0.078050986, 0.0111776795, 0.08829944, 0.032022282) * g_11;\n    result += mat4(0.01643529, 0.02285545, -0.03498564, 0.00769657, -0.0042474116, 0.015836312, -0.025771018, -0.0016368, -0.008897948, -0.012588166, -0.01416411, -0.003578984, 0.025991246, 0.021237152, 0.017450012, 0.025172485) * g_12;\n    result += mat4(0.014568868, 0.017796224, -0.036679734, -0.03138748, 0.019457601, -0.027607411, -0.004529679, -0.038048342, -0.054055385, -0.03876025, 0.041948095, 0.005869784, 0.02439633, 0.05177997, 0.016000897, 0.0057169925) * g_13;\n    result += mat4(-0.03021866, 0.017678728, -0.01371109, 0.013548159, -0.0038099394, -0.014066414, 0.028093752, 0.0027308422, -0.010615999, 0.012673458, -0.03028171, -0.016818244, -0.06530097, -0.018845048, -0.0072947564, -0.0038243714) * g_14;\n    result += mat4(-0.019006258, -0.007847591, 0.03690709, 0.06714211, 0.0073993434, -0.009766907, -0.0021441753, -0.01308625, 0.06658726, 0.06701995, -0.027305668, -0.016032105, -0.028976806, -0.0036668575, -0.0027825525, 0.0105632655) * g_15;\n    result += mat4(0.028945107, -0.0014701135, 0.048950657, -0.01923516, -0.0014054152, 0.002650635, -0.005300331, 0.004860559, 0.011158468, 0.005940625, -0.012095051, 0.0041518128, -0.020433836, -0.025870577, -0.0007547932, -0.026509356) * g_16;\n    result += mat4(-0.004545374, 0.04264545, 0.021741537, 0.029115127, 0.04225599, -0.0055392785, 0.026570829, -0.031795148, -0.008307126, 0.020176455, 0.010904648, 0.017765503, -0.10806103, -0.01776947, 0.00070428237, -0.06356262) * g_17;\n    result += mat4(-0.05663172, 0.05908046, -0.03837452, 0.06636983, -0.007960516, -0.06384041, 0.023125881, -0.030108837, 0.0038054318, -0.023263922, 0.020264054, -0.0062937695, 0.031630237, 0.020909082, 0.03594235, 0.035879835) * g_18;\n    result += mat4(-0.0050448794, 0.033650696, -0.002830413, 0.035174295, -0.024521282, 0.013054315, -0.020833842, 0.037953895, 0.08249671, 0.024239466, -0.012758333, -0.027316988, 0.051040914, 0.0005025873, 0.039778862, 0.0024668393) * g_19;\n    result += mat4(0.017232442, 0.022482058, 0.020233413, 0.024337437, 0.07986929, 0.06234036, 0.12662584, -0.05271183, -0.009718745, -0.0046989853, -0.0030333172, -0.04034237, -0.0113442, 0.022746231, -0.035293855, -0.009433693) * g_20;\n    result += mat4(0.015766997, 0.013647276, -0.029327558, 0.039106004, -0.010398323, -0.032851525, 0.02908329, -0.003789618, 0.12963496, 0.010851003, 0.1126276, -0.049255487, 0.06867432, 0.07970792, 0.017840397, -0.026481882) * g_21;\n    result += mat4(-0.058729574, -0.07886952, 0.033267397, 0.02755372, -0.0172006, 0.012404398, -0.0230168, -0.015059758, -0.09239916, -0.029533267, -0.043251917, 0.0035152994, 0.022931995, 0.101714484, -0.044946067, 0.094993) * g_22;\n    result += mat4(-0.04708704, -0.032475296, -0.03228093, -0.08810475, 0.013745045, 0.027828002, -0.031922746, 0.022986397, -0.061620213, -0.03694645, -0.055026993, 0.0031291894, -0.028799903, -0.0025357977, -0.03441407, 0.0028600092) * g_23;\n    result += mat4(0.058981724, -0.10447273, -0.088705614, 0.16546178, -0.023549391, -0.008831522, -0.018411588, 0.029640056, -0.068086684, -0.05414636, -0.029401174, 0.036180343, -0.031988926, -0.047249753, 0.008162177, 0.00548062) * g_24;\n    result += mat4(0.05287462, -0.030657746, 0.02821435, 0.037005343, 0.03534311, -0.15614955, 0.07085459, -0.11997641, -0.009156166, -0.021968868, -0.054147746, -0.07307657, -0.006428544, -0.017528288, 0.012614676, 0.037840024) * g_25;\n    result += mat4(-0.021977803, 0.047799855, 0.02660416, -0.07292106, 0.045195807, -0.0056674764, 0.10824326, -0.112114795, 0.1447127, -0.0119616175, 0.0011661504, -0.04553905, 0.13048342, 0.14574122, -0.105522245, -0.102792375) * g_26;\n    result += mat4(-0.16397473, 0.15785863, -0.06666504, -0.01682913, 0.06070918, 0.070222184, 0.037701584, 0.026657054, -0.0835267, -0.009457008, 0.13232987, 0.13508691, -0.056414206, -0.06818828, 0.079076104, 0.032249212) * g_27;\n    result += vec4(-0.10795144, -0.09953324, -0.055413827, -0.03875493);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!SAVE conv2d_last_tf1\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\n#define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.024905335, -0.0020974763, 0.02695263, 0.00016802056, -0.024053082, -0.02133723, -0.031614035, -0.031826317, 0.120421864, 0.10555479, 0.08609448, 0.116875134, 0.046175968, 0.04224941, 0.059216674, 0.035143953) * g_0;\n    result += mat4(0.059397914, 0.016519934, 0.07189327, 0.047407165, 0.04808963, 0.02792908, 0.057017103, 0.034324065, 0.14228246, 0.11275426, 0.088058695, 0.059600517, 0.02063494, 0.052596953, 0.047207687, 0.08789091) * g_1;\n    result += mat4(-0.013453174, 0.008474715, -0.017593835, 0.009218917, 0.070580654, 0.040542338, 0.08812338, 0.074653216, -0.016356857, 0.015809007, -0.008739107, 0.0097674895, -0.018381525, -0.007775341, -0.040571664, -0.011188163) * g_2;\n    result += mat4(-0.026196122, -0.034825727, -0.042998232, -0.033436514, -0.01678153, -0.004592797, -0.010311677, 0.0008815291, -0.08899181, -0.10274026, -0.066960976, -0.082430154, -0.057137426, -0.07554528, -0.030993424, -0.050372377) * g_3;\n    result += mat4(0.022921838, -0.010479244, -0.050794605, -0.073633075, -0.053708922, 0.009594084, -0.071259, -0.01054356, 0.005165821, -0.08024963, -0.049251772, -0.09581235, 0.17995799, 0.09743011, 0.13533138, 0.11643848) * g_4;\n    result += mat4(0.09727046, 0.07292666, 0.06820908, 0.041535784, -0.0049705, 0.0048759184, -0.035702795, -0.015944308, -0.010730028, 0.018847652, 0.06466244, 0.086318985, -0.05661574, -0.040698618, 0.010839972, 0.0027009705) * g_5;\n    result += mat4(-0.04628466, 0.010060396, 0.02609333, 0.08664702, 0.057045907, 0.033591177, 0.02186063, -0.024303377, 0.006569828, 0.08025825, 0.016128821, 0.10180713, -0.12228169, -0.112990454, -0.078443415, -0.09126021) * g_6;\n    result += mat4(-0.12733299, -0.087755, -0.07374111, -0.044979006, -0.025347412, -0.004083168, 0.023782173, 0.02900392, -0.017815407, -0.041119996, -0.057978686, -0.13521095, 0.08364004, 0.06950181, 0.023554614, 0.008043734) * g_7;\n    result += mat4(0.009062775, -0.003570175, -0.007378757, -0.0018487388, 0.01145638, 0.05217187, -0.008250244, 0.008433307, -0.056756936, -0.044681005, -0.08096105, -0.08033185, -0.023784965, -0.01859799, 0.013042476, 0.021188647) * g_8;\n    result += mat4(-0.0071619656, -0.012498299, -0.05144986, -0.078112476, -0.034992415, -0.017038302, -0.04464615, -0.044504963, 0.024249, -0.004297534, 0.03674578, 0.03090718, 0.04698553, 0.008344952, 0.057619847, -0.0338724) * g_9;\n    result += mat4(-0.011845145, -0.0045043705, -1.6646482e-06, -0.0038495932, -0.01992515, 0.004827126, 0.019493148, 0.00862289, 0.10151322, 0.0021909082, 0.09940764, 0.03728846, 0.027824005, 0.04358071, 0.014909185, 0.036326095) * g_10;\n    result += mat4(0.022513246, 0.028257169, 0.0102195935, 0.03301329, 0.052253865, -0.0021944977, 0.08247392, 0.03256867, -0.040685873, -0.0052207555, -0.0451257, -0.054165114, 0.01647699, 0.0028809097, -0.015233776, -0.0008741886) * g_11;\n    result += mat4(0.017371105, 0.01597189, -0.052552313, -0.008554715, -0.0023150423, 0.006076517, -0.012868931, 0.0039361073, -0.007524978, -0.004284313, -0.021520883, -0.010327569, 0.02543678, 0.008725823, -0.0073885336, 0.005528395) * g_12;\n    result += mat4(0.019192757, 0.016561812, 0.0027538154, 0.0013078215, 0.007916496, -0.042525183, -0.013173432, -0.05265476, -0.062195376, -0.011255499, 0.020898128, 0.021532273, -0.001524097, 0.034835674, -0.004051403, -0.0292426) * g_13;\n    result += mat4(-0.049191684, -9.43322e-06, -0.009106849, 0.012845289, -0.019482708, -0.011163468, 0.0034011535, -0.007062845, -0.006469714, 0.03177786, -0.033006195, -0.0006813464, -0.053963087, 0.00085209147, 0.02734121, 0.034086403) * g_14;\n    result += mat4(-0.03232248, -0.004037002, -0.010319106, 0.030889064, 0.019604538, 0.0020888883, 0.010277864, 0.000661223, 0.057915937, 0.030683514, 0.00042533095, -0.013019287, -0.015896408, 0.0038484468, -0.0042103594, 0.02174542) * g_15;\n    result += mat4(0.032975145, 0.0011456647, 0.04913679, -0.017063798, 0.0117176045, 0.007440557, 0.0020480808, 0.009415731, 0.027573857, 0.015140836, -0.01679426, -0.006124731, -0.03206279, -0.029842237, -0.010428016, -0.028513178) * g_16;\n    result += mat4(-0.00506859, 0.055869613, 0.010164368, 0.027031485, 0.042289548, -0.0054258504, 0.032214936, -0.029970925, -0.0058315448, 0.022889478, 0.01681123, 0.02985076, -0.111186065, -0.02202099, 0.0030994313, -0.062343158) * g_17;\n    result += mat4(-0.060951103, 0.06079555, -0.0396464, 0.070911355, -0.011480358, -0.06803282, 0.01637355, -0.043100975, -0.00423709, -0.028337711, 0.021635853, 0.0014857082, 0.030084312, 0.018155476, 0.043694943, 0.038795974) * g_18;\n    result += mat4(-0.0060662925, 0.029721662, -0.008117774, 0.034551267, -0.024477571, 0.018841071, -0.027095588, 0.034495078, 0.082398005, 0.008998768, -0.016399248, -0.043801688, 0.05936684, 0.006066549, 0.045399766, 3.5319943e-05) * g_19;\n    result += mat4(0.019259382, 0.02494012, 0.029301709, 0.028329274, 0.09122267, 0.06900443, 0.1412115, -0.043169618, -0.01627418, -0.004989528, -0.0042651827, -0.04556752, -0.023623291, 0.013007996, -0.04483056, -0.015727345) * g_20;\n    result += mat4(0.016332543, 0.016384754, -0.030676385, 0.045312885, -0.0100853555, -0.032632045, 0.031514473, -0.0070776115, 0.13642761, 0.0023589598, 0.12214136, -0.062155515, 0.08240989, 0.08894205, 0.03325406, -0.016589595) * g_21;\n    result += mat4(-0.06494277, -0.08158925, 0.030425413, 0.019835634, -0.012624623, 0.013942616, -0.030527417, -0.021668324, -0.09444672, -0.033064254, -0.044167448, 0.0011024752, 0.03210801, 0.12662941, -0.03912534, 0.1112649) * g_22;\n    result += mat4(-0.04716062, -0.03751481, -0.031030515, -0.09067383, 0.0077815712, 0.02169541, -0.035285182, 0.02290573, -0.0704085, -0.03916127, -0.058103334, 0.004915147, -0.0333844, -0.011548617, -0.031151932, -0.00043817286) * g_23;\n    result += mat4(0.05976319, -0.107285, -0.097245865, 0.17706421, -0.021453341, -0.0047738464, -0.017621001, 0.033400454, -0.07225561, -0.05599672, -0.027600193, 0.038664024, -0.03762786, -0.052429967, 0.0104017975, 0.007116869) * g_24;\n    result += mat4(0.06014114, -0.029824806, 0.03209269, 0.04392036, 0.031300627, -0.16249833, 0.06878509, -0.12658615, -0.012383169, -0.025043553, -0.06527381, -0.08149099, -0.014006842, -0.018669648, 0.014510818, 0.042045828) * g_25;\n    result += mat4(-0.023342922, 0.047104675, 0.029629575, -0.082307704, 0.04035797, -0.0013049254, 0.11085582, -0.11031226, 0.14778149, -0.016699014, -0.00634342, -0.055320874, 0.14306462, 0.15896587, -0.110229075, -0.1069649) * g_26;\n    result += mat4(-0.17449625, 0.15787153, -0.06711028, -0.023110518, 0.06862914, 0.074063435, 0.042682912, 0.029800726, -0.08768606, -0.009814701, 0.14180017, 0.14780663, -0.05672417, -0.074305914, 0.07873489, 0.028458012) * g_27;\n    result += vec4(0.06026231, 0.040204916, 0.037672628, 0.023496555);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112\n//!HOOK MAIN\n//!BIND conv2d_tf\n//!BIND conv2d_tf1\n//!BIND conv2d_1_tf\n//!BIND conv2d_1_tf1\n//!BIND conv2d_2_tf\n//!BIND conv2d_2_tf1\n//!BIND conv2d_3_tf\n//!BIND conv2d_3_tf1\n//!BIND conv2d_4_tf\n//!BIND conv2d_4_tf1\n//!BIND conv2d_5_tf\n//!BIND conv2d_5_tf1\n//!BIND conv2d_6_tf\n//!BIND conv2d_6_tf1\n//!SAVE conv2d_last_tf2\n//!WIDTH conv2d_tf.w\n//!HEIGHT conv2d_tf.h\n//!COMPONENTS 4\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\n#define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0))\n#define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0))\n#define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0))\n#define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0))\n#define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0))\n#define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0))\n#define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0))\n#define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0))\n#define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0))\n#define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0))\n#define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0))\n#define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0))\n#define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\n#define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0))\n#define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0))\nvec4 hook() {\n    vec4 result = mat4(0.1765669, 0.14268716, 0.19186598, 0.15799578, 0.016374417, 0.018578433, 0.0039475, 0.0046772263, 0.39840183, 0.36909792, 0.35409746, 0.37422222, -0.108508386, -0.1331279, -0.10336035, -0.14776541) * g_0;\n    result += mat4(-0.057757027, -0.14071062, -0.025283009, -0.09397916, -0.09031894, -0.14219165, -0.08299535, -0.13970287, -0.12259208, -0.14382727, -0.22002274, -0.25016093, -0.048906635, 0.06620249, 0.016965045, 0.1295978) * g_1;\n    result += mat4(-0.16748372, -0.13718611, -0.18565705, -0.15029612, -0.080749065, -0.09955825, 0.032431383, 0.023855643, -0.2748885, -0.23232168, -0.29121292, -0.26405892, 0.16556135, 0.18657646, 0.1424068, 0.18855052) * g_2;\n    result += mat4(0.10960496, 0.10851629, 0.095003806, 0.11053746, 0.09885307, 0.14437789, 0.13191165, 0.17365928, 0.16558935, 0.15473324, 0.21136154, 0.19976667, -0.07267957, -0.11469687, -0.029134216, -0.06817615) * g_3;\n    result += mat4(0.10202856, 0.04216857, -0.03959349, -0.09849683, -0.1576996, -0.049997438, -0.1579918, -0.058789205, 0.029792828, -0.07311781, -0.045432188, -0.11312683, 0.24257647, 0.16204113, 0.17869382, 0.16024388) * g_4;\n    result += mat4(0.17193612, 0.12692013, 0.13177487, 0.0796725, 0.0797928, 0.08952722, -0.012468046, 0.011071511, -0.068559825, -0.024852324, 0.0526428, 0.07917346, -0.085534215, -0.09591339, 0.04615827, 0.024577664) * g_5;\n    result += mat4(-0.14653449, -0.067267366, -0.002524394, 0.086243175, 0.13660401, 0.08039592, 0.09179008, 0.022573143, -0.024744196, 0.09120211, 0.017654825, 0.14114714, -0.16093308, -0.14538004, -0.09950235, -0.111152865) * g_6;\n    result += mat4(-0.188637, -0.12968326, -0.1200479, -0.06537649, -0.12589337, -0.106242515, -0.02788782, -0.025949068, 0.04948153, 0.02222735, -0.025291357, -0.12379292, 0.11074645, 0.11902375, -0.00056989543, -0.0024386419) * g_7;\n    result += mat4(0.018286629, 0.0072215167, 0.00037828335, 0.0047001047, 0.011478272, 0.041745186, -0.015742473, -0.002282524, -0.03440817, -0.02196847, -0.07838253, -0.07993771, -0.010155526, -0.017590692, 0.027141469, 0.029741213) * g_8;\n    result += mat4(0.016512005, 0.004950637, -0.0238836, -0.05587327, -0.03164328, -0.009499985, -0.059880238, -0.061794154, 0.023154303, -0.013266373, 0.04701534, 0.0415862, 0.06357814, 0.033057794, 0.08389772, 0.00035060212) * g_9;\n    result += mat4(-0.016403968, -0.012538788, -0.0015746636, -0.004771009, -0.021361275, -0.009695242, 0.020548422, -0.0024130535, 0.07796766, -0.01516671, 0.09961382, 0.042754963, 0.017363647, 0.03729065, -0.004795824, 0.01550197) * g_10;\n    result += mat4(-0.0028093113, 0.011869523, -0.02216933, 0.011177349, 0.033342455, -0.021146454, 0.07830085, 0.032490104, -0.03281833, 0.0060484232, -0.04081057, -0.04945058, -0.0056189033, -0.010636801, -0.041949317, -0.025739705) * g_11;\n    result += mat4(0.012979897, 0.016758928, -0.049062215, -0.0035748442, 0.0085972, 0.0036381132, -0.0055621094, 0.0041307937, -0.0008907763, -0.0034079372, -0.025680453, -0.015531803, 0.012816766, 0.009977763, -0.016416566, 0.0034859509) * g_12;\n    result += mat4(0.021753248, 0.016452711, 0.009833835, 0.0065052663, 0.0014061348, -0.046160888, -0.0132271005, -0.05051269, -0.05746351, -0.0012690664, 0.017191738, 0.018192926, -0.008879476, 0.026354216, -0.012801991, -0.029587373) * g_13;\n    result += mat4(-0.04220692, -0.0015560482, -0.0019648245, 0.013402305, -0.018259782, -0.0036008905, 0.0035650074, -0.0019178417, 0.00051580026, 0.027355857, -0.017914988, 0.004937948, -0.046335887, 0.00013612259, 0.030293299, 0.030688645) * g_14;\n    result += mat4(-0.036683388, -0.0031274238, -0.026074665, 0.021684237, 0.022639066, 0.0022493738, 0.011508554, -0.0006385944, 0.04890418, 0.020119468, 0.004167364, -0.008356099, -0.008598796, 0.0089028, -0.0029575853, 0.016687104) * g_15;\n    result += mat4(0.027207986, 0.0011099194, 0.042383645, -0.015179333, 0.014744431, 0.006148344, 0.005165422, 0.0070196544, 0.030286826, 0.016620956, -0.01611366, -0.00667594, -0.029524863, -0.024751091, -0.013321004, -0.025199674) * g_16;\n    result += mat4(0.0027477827, 0.054622147, 0.010154094, 0.025437292, 0.031773083, -0.01055473, 0.022864206, -0.029010754, -0.0029999653, 0.025018329, 0.015316208, 0.027188798, -0.10096525, -0.017268656, 0.0012529213, -0.062078856) * g_17;\n    result += mat4(-0.053670805, 0.057336535, -0.037418038, 0.06443577, -0.016027879, -0.058168363, 0.007034215, -0.03390141, -0.0019346164, -0.027947908, 0.021723913, -0.0018286633, 0.030507812, 0.018293543, 0.042917266, 0.033528328) * g_18;\n    result += mat4(-0.004559579, 0.029667616, -0.001870353, 0.0378995, -0.017147437, 0.020192018, -0.021574946, 0.031568103, 0.07487145, 0.0032376775, -0.018893708, -0.041981626, 0.054478757, 0.0061423797, 0.041280247, 0.000878061) * g_19;\n    result += mat4(0.017076394, 0.023647636, 0.029403262, 0.029923365, 0.08866472, 0.060613394, 0.1314274, -0.04490231, -0.016304834, -0.0062647443, -0.0031828512, -0.03989252, -0.024330825, 0.00741213, -0.04075287, -0.01615817) * g_20;\n    result += mat4(0.017866978, 0.017720113, -0.02846163, 0.040761847, -0.0063438355, -0.02347501, 0.029564403, -0.0029562064, 0.12505588, -0.0073986333, 0.11250363, -0.06179967, 0.07854423, 0.08546533, 0.034743227, -0.010757377) * g_21;\n    result += mat4(-0.06416677, -0.08344284, 0.030138884, 0.017635904, -0.012087523, 0.014205202, -0.03221233, -0.023834767, -0.091186255, -0.028958676, -0.04724334, 0.00013161585, 0.027391518, 0.1249978, -0.045047652, 0.10737729) * g_22;\n    result += mat4(-0.04326348, -0.03543181, -0.029558217, -0.08582413, 0.007812453, 0.014296562, -0.028779754, 0.018517692, -0.063755795, -0.036619596, -0.050809663, 0.005431336, -0.029205568, -0.011827915, -0.031110523, -0.005648626) * g_23;\n    result += mat4(0.05499293, -0.10000709, -0.0943537, 0.16143042, -0.019952895, -0.0039807972, -0.014841254, 0.0320363, -0.065173544, -0.049425576, -0.023904482, 0.03759679, -0.03207411, -0.047782745, 0.01352581, 0.008140566) * g_24;\n    result += mat4(0.055923894, -0.025134467, 0.029583648, 0.04096879, 0.027551858, -0.14995384, 0.06467113, -0.11633077, -0.01563784, -0.026909819, -0.06292879, -0.078409635, -0.009081105, -0.015533088, 0.019585673, 0.04334208) * g_25;\n    result += mat4(-0.021717606, 0.042464726, 0.02743202, -0.07388838, 0.03460472, 0.0038285658, 0.099842004, -0.098247, 0.13276267, -0.020793032, -0.008603039, -0.051913783, 0.12959045, 0.14735717, -0.10888226, -0.10263746) * g_26;\n    result += mat4(-0.16819532, 0.141579, -0.062480718, -0.021918943, 0.06348125, 0.06849444, 0.03888676, 0.027375204, -0.08194279, -0.012574497, 0.13523251, 0.13739482, -0.047547445, -0.058767617, 0.07009549, 0.028136581) * g_27;\n    result += vec4(0.069033325, 0.040207114, 0.027286075, 0.0065334598);\n    return result;\n}\n//!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Depth-to-Space\n//!HOOK MAIN\n//!BIND MAIN\n//!BIND conv2d_last_tf\n//!BIND conv2d_last_tf1\n//!BIND conv2d_last_tf2\n//!SAVE MAIN\n//!WIDTH conv2d_last_tf.w 2 *\n//!HEIGHT conv2d_last_tf.h 2 *\n//!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > *\nvec4 hook() {\n    vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size);\n    ivec2 i0 = ivec2(f0 * vec2(2.0));\n    float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x];\n    vec2 f1 = fract(conv2d_last_tf1_pos * conv2d_last_tf1_size);\n    ivec2 i1 = ivec2(f1 * vec2(2.0));\n    float c1 = conv2d_last_tf1_tex((vec2(0.5) - f1) * conv2d_last_tf1_pt + conv2d_last_tf1_pos)[i1.y * 2 + i1.x];\n    vec2 f2 = fract(conv2d_last_tf2_pos * conv2d_last_tf2_size);\n    ivec2 i2 = ivec2(f2 * vec2(2.0));\n    float c2 = conv2d_last_tf2_tex((vec2(0.5) - f2) * conv2d_last_tf2_pt + conv2d_last_tf2_pos)[i2.y * 2 + i2.x];\n    float c3 = c2;\n    return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos);\n}\n"
  },
  {
    "path": "assets/shaders/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 bloc97\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": "assets/statements/statements.txt",
    "content": "在使用本软件之前，请您仔细阅读以下内容，并确保您充分理解并同意以下条款：\n1、本软件为开源软件，您应该免费获取和使用。如果您是从第三方付费获取，建议您向其索取赔偿。\n2、本软件完全基于您个人意愿使用，您应该对自己的使用行为和所有结果承担全部责任。\n3、本软件仅供学习交流、科研等非商业性质的用途，严禁将本软件用于商业目的。如有任何商业行为，均与本软件无关。\n4、本软件并不保证与所有操作系统或硬件设备兼容。本软件作者或贡献者不对因使用本软件而产生的任何技术或安全问题承担责任。\n5、本软件作者或贡献者不承担因使用本软件而造成的任何直接、间接、特殊或后果性的损失或损害的责任，包括但不限于财产损失、商业利润损失、信息或数据丢失或损坏等。\n6、本软件使用者应遵守国家相关法律法规和使用规范，不得利用本软件从事任何违法违规行为。如因使用本软件而导致的违法行为，使用者应承担相应的法律责任。\n7、本软件不会收集、存储、使用任何用户的个人信息，包括但不限于姓名、地址、电子邮件地址、电话号码等。在使用本软件过程中，不会进行任何形式的个人信息采集。\n8、本软件作者或贡献者保留随时修改、增加、删除本免责声明中的内容而不另行通知的权利。\n9、如果本软件存在侵犯您的合法权益的情况，请及时与作者联系，作者将会及时删除有关内容。\n如您不同意本免责声明中的任何内容，请勿使用本软件。使用本软件即代表您已完全理解并同意上述内容。"
  },
  {
    "path": "devtools_options.yaml",
    "content": "extensions:\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "A flutter app for collecting and watching anime online with custom rules. Use up to five lines of Xpath expressions to build your own rules. Supports rule import and rule sharing. Supports danmaku. In development (～￣▽￣)～"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "An anime collection APP based on custom rules."
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Kazumi"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/full_description.txt",
    "content": "使用 flutter 开发的基于自定义规则的番剧采集与在线观看程序。使用最多五行基于 Xpath 语法的选择器构建自己的规则。支持规则导入与规则分享。绝赞开发中 (～￣▽￣)～"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/short_description.txt",
    "content": "基于自定义规则的番剧采集APP，支持流媒体在线观看，支持弹幕。"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/title.txt",
    "content": "Kazumi"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>13.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\n# platform :ios, '13.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\nimport AVKit\n\n@main\n@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {\n\n    override func application(\n        _ application: UIApplication,\n        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n    ) -> Bool {\n        return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n    }\n\n    func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {\n        GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)\n\n        let channel = FlutterMethodChannel(\n            name: \"com.predidit.kazumi/intent\",\n            binaryMessenger: engineBridge.applicationRegistrar.messenger()\n        )\n        channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in\n            if call.method == \"openWithReferer\" {\n                guard let args = call.arguments else { return }\n                if let myArgs = args as? [String: Any],\n                   let url = myArgs[\"url\"] as? String,\n                   let referer = myArgs[\"referer\"] as? String {\n                    self?.openVideoWithReferer(url: url, referer: referer)\n                }\n                result(nil)\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        }\n\n        let storageChannel = FlutterMethodChannel(\n            name: \"com.predidit.kazumi/storage\",\n            binaryMessenger: engineBridge.applicationRegistrar.messenger()\n        )\n        storageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in\n            if call.method == \"getAvailableStorage\" {\n                do {\n                    let attrs = try FileManager.default.attributesOfFileSystem(\n                        forPath: NSHomeDirectory()\n                    )\n                    if let freeSize = attrs[.systemFreeSize] as? Int64 {\n                        result(freeSize)\n                    } else {\n                        result(-1)\n                    }\n                } catch {\n                    result(-1)\n                }\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        }\n    }\n    \n    // TODO: ADD VLC SUPPORT\n    // VLC can be downloaded from iOS App Store, but don't know how to build selectable app lists, while checking if it is installled.\n    // VLC supports more video formats than AVPlayer but does not support referer while AVPlayer does\n    private func openVideoWithReferer(url: String, referer: String) {\n        guard let videoUrl = URL(string: url) else { return }\n\n        let headers: [String: String] = [\n            \"Referer\": referer,\n        ]\n        let asset = AVURLAsset(url: videoUrl, options: [\"AVURLAssetHTTPHeaderFieldsKey\": headers])\n        let playerItem = AVPlayerItem(asset: asset)\n        let player = AVPlayer(playerItem: playerItem)\n        let playerViewController = AVPlayerViewController()\n        playerViewController.player = player\n        playerViewController.videoGravity = AVLayerVideoGravity.resizeAspect\n\n        // Use UIScene API instead of deprecated keyWindow\n        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,\n              let rootViewController = windowScene.windows.first?.rootViewController else {\n            return\n        }\n\n        rootViewController.present(playerViewController, animated: true) {\n            playerViewController.player?.play()\n        }\n\n//        guard let appURL = URL(string: \"vlc-x-callback://x-callback-url/stream?url=\" + url) else {\n//            return\n//        }\n//        if UIApplication.shared.canOpenURL(appURL) && referer.isEmpty {\n//            UIApplication.shared.open(appURL, options: [:], completionHandler: nil)\n//        }\n    }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"background.png\",\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"darkbackground.png\",\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"LaunchImage.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"LaunchImageDark.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"LaunchImageDark@2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"LaunchImageDark@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"scaleToFill\" image=\"LaunchBackground\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"tWc-Dq-wcI\"/>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\"></imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"3T2-ad-Qdv\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"bottom\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" id=\"RPx-PI-7Xg\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"top\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"top\" id=\"SdS-ul-q2q\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"tWc-Dq-wcI\" secondAttribute=\"trailing\" id=\"Swv-Gf-Rwn\"/>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"YRO-k0-Ey4\" secondAttribute=\"trailing\" id=\"TQA-XW-tRk\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"bottom\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" id=\"duK-uY-Gun\"/>\n                            <constraint firstItem=\"tWc-Dq-wcI\" firstAttribute=\"leading\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"leading\" id=\"kV7-tw-vXt\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"top\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"top\" id=\"xPn-NY-SIU\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"1024\" height=\"1024\"/>\n        <image name=\"LaunchBackground\" width=\"1\" height=\"1\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string></string>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Kazumi</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>kazumi</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSApplicationQueriesSchemes</key>\n\t<array>\n\t\t<string>vlc-x-callback</string>\n\t</array>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<true/>\n\t</dict>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UIApplicationSceneManifest</key>\n\t<dict>\n\t\t<key>UIApplicationSupportsMultipleScenes</key>\n\t\t<false/>\n\t\t<key>UISceneConfigurations</key>\n\t\t<dict>\n\t\t\t<key>UIWindowSceneSessionRoleApplication</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>UISceneClassName</key>\n\t\t\t\t\t<string>UIWindowScene</string>\n\t\t\t\t\t<key>UISceneDelegateClassName</key>\n\t\t\t\t\t<string>FlutterSceneDelegate</string>\n\t\t\t\t\t<key>UISceneConfigurationName</key>\n\t\t\t\t\t<string>flutter</string>\n\t\t\t\t\t<key>UISceneStoryboardFile</key>\n\t\t\t\t\t<string>Main</string>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t</dict>\n\t</dict>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIStatusBarHidden</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "lib/app_module.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/index_module.dart';\n\nclass AppModule extends Module {\n  @override\n  void binds(i) {\n\n  }\n\n  @override\n  void routes(r) {\n    r.module(\"/\", module: IndexModule());\n  }\n}"
  },
  {
    "path": "lib/app_widget.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:dynamic_color/dynamic_color.dart';\nimport 'package:flutter_displaymode/flutter_displaymode.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:tray_manager/tray_manager.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/settings/theme_provider.dart';\nimport 'package:provider/provider.dart';\nimport 'package:kazumi/utils/constants.dart';\n\nclass AppWidget extends StatefulWidget {\n  const AppWidget({super.key});\n\n  @override\n  State<AppWidget> createState() => _AppWidgetState();\n}\n\nclass _AppWidgetState extends State<AppWidget>\n    with TrayListener, WidgetsBindingObserver, WindowListener {\n  Box setting = GStorage.setting;\n\n  final TrayManager trayManager = TrayManager.instance;\n  bool showingExitDialog = false;\n\n  @override\n  void initState() {\n    trayManager.addListener(this);\n    windowManager.addListener(this);\n    setPreventClose();\n    WidgetsBinding.instance.addObserver(this);\n    super.initState();\n  }\n\n  void setPreventClose() async {\n    if (Utils.isDesktop()) {\n      await windowManager.setPreventClose(true);\n      setState(() {});\n    }\n  }\n\n  @override\n  void dispose() {\n    trayManager.removeListener(this);\n    windowManager.removeListener(this);\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n\n  @override\n  void onTrayIconMouseDown() {\n    windowManager.show();\n  }\n\n  @override\n  void onTrayIconRightMouseDown() {\n    trayManager.popUpContextMenu();\n  }\n\n  @override\n  void onTrayMenuItemClick(MenuItem menuItem) {\n    switch (menuItem.key) {\n      case 'show_window':\n        windowManager.show();\n      case 'exit':\n        exit(0);\n    }\n  }\n\n  /// 处理窗口关闭事件，\n  /// 需要使用 `windowManager.close()` 来触发，`exit(0)` 会直接退出程序\n  @override\n  void onWindowClose() {\n    final setting = GStorage.setting;\n    final exitBehavior =\n        setting.get(SettingBoxKey.exitBehavior, defaultValue: 2);\n\n    switch (exitBehavior) {\n      case 0:\n        exit(0);\n      case 1:\n        KazumiDialog.dismiss();\n        windowManager.hide();\n        break;\n      default:\n        if (showingExitDialog) return;\n        showingExitDialog = true;\n        KazumiDialog.show(onDismiss: () {\n          showingExitDialog = false;\n        }, builder: (context) {\n          bool saveExitBehavior = false; // 下次不再询问？\n\n          return AlertDialog(\n            title: const Text('退出确认'),\n            content: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.stretch,\n              children: [\n                const Text('您想要退出 Kazumi 吗？'),\n                const SizedBox(height: 24),\n                StatefulBuilder(builder: (context, setState) {\n                  onChanged(value) {\n                    saveExitBehavior = value ?? false;\n                    setState(() {});\n                  }\n\n                  return Wrap(\n                    crossAxisAlignment: WrapCrossAlignment.center,\n                    spacing: 8,\n                    children: [\n                      Checkbox(value: saveExitBehavior, onChanged: onChanged),\n                      const Text('下次不再询问'),\n                    ],\n                  );\n                }),\n              ],\n            ),\n            actions: [\n              TextButton(\n                  onPressed: () async {\n                    if (saveExitBehavior) {\n                      await setting.put(SettingBoxKey.exitBehavior, 0);\n                    }\n                    exit(0);\n                  },\n                  child: const Text('退出 Kazumi')),\n              TextButton(\n                  onPressed: () async {\n                    if (saveExitBehavior) {\n                      await setting.put(SettingBoxKey.exitBehavior, 1);\n                    }\n                    KazumiDialog.dismiss();\n                    windowManager.hide();\n                  },\n                  child: const Text('最小化至托盘')),\n              const TextButton(\n                  onPressed: KazumiDialog.dismiss, child: Text('取消')),\n            ],\n          );\n        });\n    }\n  }\n\n  /// 处理前后台变更\n  /// windows/linux 在程序后台或失去焦点时只会触发 inactive 不会触发 paused\n  /// android/ios/macos 在程序后台时会先触发 inactive 再触发 paused, 回到前台时会先触发 inactive 再触发 resumed\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) async {\n    super.didChangeAppLifecycleState(state);\n    if (state == AppLifecycleState.paused) {\n      KazumiLogger()\n          .i(\"AppLifecycleState.paused: Application moved to background\");\n    } else if (state == AppLifecycleState.resumed) {\n      KazumiLogger()\n          .i(\"AppLifecycleState.resumed: Application moved to foreground\");\n    } else if (state == AppLifecycleState.inactive) {\n      KazumiLogger().i(\"AppLifecycleState.inactive: Application is inactive\");\n    }\n  }\n\n  Future<void> _handleTray() async {\n    if (Platform.isWindows) {\n      await trayManager.setIcon('assets/images/logo/logo_lanczos.ico');\n    } else if (Platform.environment.containsKey('FLATPAK_ID') ||\n        Platform.environment.containsKey('SNAP')) {\n      await trayManager.setIcon('io.github.Predidit.Kazumi');\n    } else {\n      await trayManager.setIcon('assets/images/logo/logo_rounded.png');\n    }\n\n    if (!Platform.isLinux) {\n      await trayManager.setToolTip('Kazumi');\n    }\n\n    Menu trayMenu = Menu(items: [\n      MenuItem(key: 'show_window', label: '显示窗口'),\n      MenuItem.separator(),\n      MenuItem(key: 'exit', label: '退出 Kazumi')\n    ]);\n    await trayManager.setContextMenu(trayMenu);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeProvider themeProvider = Provider.of<ThemeProvider>(context);\n    if (Utils.isDesktop()) {\n      _handleTray();\n    }\n    dynamic color;\n    dynamic defaultThemeColor =\n        setting.get(SettingBoxKey.themeColor, defaultValue: 'default');\n    if (defaultThemeColor == 'default') {\n      color = Colors.green;\n    } else {\n      color = Color(int.parse(defaultThemeColor, radix: 16));\n    }\n    bool oledEnhance =\n        setting.get(SettingBoxKey.oledEnhance, defaultValue: false);\n    bool useSystemFont =\n        setting.get(SettingBoxKey.useSystemFont, defaultValue: false);\n    final defaultThemeMode =\n        setting.get(SettingBoxKey.themeMode, defaultValue: 'system');\n    if (defaultThemeMode == 'dark') {\n      themeProvider.setThemeMode(ThemeMode.dark, notify: false);\n    }\n    if (defaultThemeMode == 'light') {\n      themeProvider.setThemeMode(ThemeMode.light, notify: false);\n    }\n    if (defaultThemeMode == 'system') {\n      themeProvider.setThemeMode(ThemeMode.system, notify: false);\n    }\n    themeProvider.setFontFamily(useSystemFont, notify: false);\n    var defaultDarkTheme = ThemeData(\n        useMaterial3: true,\n        fontFamily: themeProvider.currentFontFamily,\n        brightness: Brightness.dark,\n        colorSchemeSeed: color,\n        progressIndicatorTheme: progressIndicatorTheme2024,\n        sliderTheme: sliderTheme2024,\n        pageTransitionsTheme: pageTransitionsTheme2024);\n    var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme);\n    themeProvider.setTheme(\n      ThemeData(\n          useMaterial3: true,\n          fontFamily: themeProvider.currentFontFamily,\n          brightness: Brightness.light,\n          colorSchemeSeed: color,\n          progressIndicatorTheme: progressIndicatorTheme2024,\n          sliderTheme: sliderTheme2024,\n          pageTransitionsTheme: pageTransitionsTheme2024),\n      oledEnhance ? oledDarkTheme : defaultDarkTheme,\n      notify: false,\n    );\n    var app = DynamicColorBuilder(\n      builder: (theme, darkTheme) {\n        if (themeProvider.useDynamicColor) {\n          themeProvider.setTheme(\n            ThemeData(\n                useMaterial3: true,\n                fontFamily: themeProvider.currentFontFamily,\n                colorScheme: theme,\n                brightness: Brightness.light,\n                progressIndicatorTheme: progressIndicatorTheme2024,\n                sliderTheme: sliderTheme2024,\n                pageTransitionsTheme: pageTransitionsTheme2024),\n            oledEnhance\n                ? Utils.oledDarkTheme(ThemeData(\n                    useMaterial3: true,\n                    fontFamily: themeProvider.currentFontFamily,\n                    colorScheme: darkTheme,\n                    brightness: Brightness.dark,\n                    progressIndicatorTheme: progressIndicatorTheme2024,\n                    sliderTheme: sliderTheme2024,\n                    pageTransitionsTheme: pageTransitionsTheme2024))\n                : ThemeData(\n                    useMaterial3: true,\n                    fontFamily: themeProvider.currentFontFamily,\n                    colorScheme: darkTheme,\n                    brightness: Brightness.dark,\n                    progressIndicatorTheme: progressIndicatorTheme2024,\n                    sliderTheme: sliderTheme2024,\n                    pageTransitionsTheme: pageTransitionsTheme2024),\n            notify: false,\n          );\n        }\n        return MaterialApp.router(\n          title: \"Kazumi\",\n          localizationsDelegates: GlobalMaterialLocalizations.delegates,\n          supportedLocales: const [\n            Locale.fromSubtags(\n                languageCode: 'zh', scriptCode: 'Hans', countryCode: \"CN\")\n          ],\n          locale: const Locale.fromSubtags(\n              languageCode: 'zh', scriptCode: 'Hans', countryCode: \"CN\"),\n          theme: themeProvider.light,\n          darkTheme: themeProvider.dark,\n          themeMode: themeProvider.themeMode,\n          routerConfig: Modular.routerConfig,\n        );\n      },\n    );\n    Modular.setObservers([KazumiDialog.observer]);\n\n    // 强制设置高帧率\n    if (Platform.isAndroid) {\n      try {\n        late List modes;\n        FlutterDisplayMode.supported.then((value) {\n          modes = value;\n          var storageDisplay = setting.get(SettingBoxKey.displayMode);\n          DisplayMode f = DisplayMode.auto;\n          if (storageDisplay != null) {\n            f = modes.firstWhere((e) => e.toString() == storageDisplay);\n          }\n          DisplayMode preferred = modes.toList().firstWhere((el) => el == f);\n          FlutterDisplayMode.setPreferredMode(preferred);\n        });\n      } catch (e) {\n        KazumiLogger().e('DisPlay: set preferred mode failed', error: e);\n      }\n    }\n\n    return app;\n  }\n}\n"
  },
  {
    "path": "lib/bbcode/README.md",
    "content": "# 基于 antlr4 的 BBCode 解析\n\n## 相关文件\n\n- [assets/bbcode/BBCode.g4](../../assets/bbcode/BBCode.g4): antlr4 语法文件\n- [lib/bbcode/generated](../../lib/bbcode/generated): antlr4 生成的 dart 代码所在文件夹\n\n## 关键文件\n\n- [lib/bbcode/bbcode_elements.dart](bbcode_elements.dart): BBCode 元素\n- [lib/bbcode/bbcode_base_listener.dart](bbcode_base_listener.dart): BBCode 解析器的入口文件\n- [lib/bbcode/bbcode_widget.dart](bbcode_widget.dart): BBCode 组件\n\n## 如何开发\n\n### 配置环境\n\n1. 根据[官方文档](https://github.com/antlr/antlr4/blob/dev/doc/dart-target.md)配置环境\n2. 在 IDE 中安装 `antlr v4` 插件\n\n### 开发\n\n1. 修改 [assets/bbcode/BBCode.g4](../../assets/bbcode/BBCode.g4) 文件，通过插件的 Preview 功能确定解析是否正确\n2. 通过该文件生成新的 dart 文件到 [lib/bbcode/generated](../../lib/bbcode/generated) 文件夹内，删除无用文件\n3. 参考文件内的注释进行修改\n\n### 测试 BBCode\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'bbcode/bbcode_widget.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        appBar: AppBar(title: const Text('BBCode Parser')),\n        body: Card(\n          color: Theme.of(context).colorScheme.secondaryContainer,\n          child: const Padding(\n            padding: EdgeInsets.all(8.0),\n            child: Padding(\n              padding: EdgeInsets.all(16),\n              child: BBCodeWidget(\n                bbcode:\n                '[quote][b]用户[/b]说：[s]测试表情和删除线(bgm35)[/s][/quote]\\n[mask]测试特殊符号[]()测试字符表情(TAT)[/mask][url=https://bangumi.tv/blog/348736]测试链接[/url][url]https://bangumi.tv/blog/348736[/url][img]https://bangumi.tv/img/rc3/logo_2x.png[/img]\\n\\n[color=grey][size=10][来自Bangumi for android] [url=https://bgm.tv/group/topic/350677][color=grey]获取[/color][/url][/size][/color]',\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n"
  },
  {
    "path": "lib/bbcode/bbcode_base_listener.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:antlr4/antlr4.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/bbcode/bbcode_elements.dart';\n\nimport 'generated/BBCodeListener.dart';\nimport 'generated/BBCodeParser.dart';\n\nclass BBCodeBaseListener implements BBCodeListener {\n  final List<dynamic> bbcode = [];\n\n  /// 记录进入标签时的位置\n  void _enterTag(TagContext ctx) {\n    final tagName = ctx.tagName?.text;\n\n    switch (tagName) {\n      case 'URL':\n      case 'url':\n        bbCodeTag.link = bbcode.length;\n        break;\n      case 'USER':\n      case 'user':\n        bbCodeTag.link = bbcode.length;\n        break;\n      case 'QUOTE':\n      case 'quote':\n        bbCodeTag.quoted = bbcode.length;\n        break;\n      case 'B':\n      case 'b':\n        bbCodeTag.bold = bbcode.length;\n        break;\n      case 'I':\n      case 'i':\n        bbCodeTag.italic = bbcode.length;\n        break;\n      case 'S':\n      case 's':\n        bbCodeTag.strikeThrough = bbcode.length;\n        break;\n      case 'U':\n      case 'u':\n        bbCodeTag.underline = bbcode.length;\n        break;\n      case 'PHOTO':\n      case 'photo':\n      case 'IMG':\n      case 'img':\n        bbCodeTag.img = bbcode.length;\n        break;\n      case 'MASK':\n      case 'mask':\n        bbCodeTag.masked = bbcode.length;\n        break;\n      case 'SIZE':\n      case 'size':\n        bbCodeTag.size = bbcode.length;\n        break;\n      case 'COLOR':\n      case 'color':\n        bbCodeTag.color = bbcode.length;\n        break;\n      default:\n        KazumiLogger()\n            .e('BBCode: unrecognized Tag: ${ctx.text}, please submit an issue with logs, bangumi, and episode information');\n        break;\n    }\n  }\n\n  /// 对标签内所有的 BBCodeText 叠加样式\n  void _exitTag(TagContext ctx) {\n    final tagName = ctx.tagName?.text;\n\n    switch (tagName) {\n      case 'URL':\n      case 'url':\n        if (bbcode.isNotEmpty && bbcode[bbCodeTag.link!] is BBCodeText) {\n          if (ctx.attr != null) {\n            bbcode[bbCodeTag.link!].link = ctx.attr!.text;\n          } else {\n            bbcode[bbCodeTag.link!].link = bbcode[bbCodeTag.link!].text;\n          }\n        }\n        break;\n      case 'USER':\n      case 'user':\n        if (bbcode.isNotEmpty &&\n            ctx.attr != null &&\n            bbcode[bbCodeTag.link!] is BBCodeText) {\n          bbcode[bbCodeTag.link!].link =\n              'https://bangumi.tv/user/${ctx.attr!.text}';\n          bbcode[bbCodeTag.link!].text = '@${bbcode[bbCodeTag.link!].text}';\n        }\n        break;\n      case 'QUOTE':\n      case 'quote':\n        for (int i = bbCodeTag.quoted!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].quoted = true;\n          }\n        }\n        // Add icon to the end of quoted text\n        bbcode.add(const Icon(Icons.format_quote));\n        break;\n      case 'B':\n      case 'b':\n        for (int i = bbCodeTag.bold!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].bold = true;\n          }\n        }\n        break;\n      case 'I':\n      case 'i':\n        for (int i = bbCodeTag.italic!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].italic = true;\n          }\n        }\n        break;\n      case 'S':\n      case 's':\n        for (int i = bbCodeTag.strikeThrough!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].strikeThrough = true;\n          }\n        }\n        break;\n      case 'U':\n      case 'u':\n        for (int i = bbCodeTag.underline!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].underline = true;\n          }\n        }\n        break;\n      case 'PHOTO':\n      case 'photo':\n      case 'IMG':\n      case 'img':\n        if (bbCodeTag.img! < bbcode.length &&\n            bbcode.isNotEmpty &&\n            bbcode[bbCodeTag.img!] is BBCodeText) {\n          bbcode[bbCodeTag.img!] =\n              BBCodeImg(imageUrl: bbcode[bbCodeTag.img!].text);\n        }\n        break;\n      case 'MASK':\n      case 'mask':\n        for (int i = bbCodeTag.masked!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].masked = true;\n          }\n        }\n        break;\n      case 'SIZE':\n      case 'size':\n        for (int i = bbCodeTag.size!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].size = int.parse(ctx.attr!.text!);\n          }\n        }\n        break;\n      case 'COLOR':\n      case 'color':\n        for (int i = bbCodeTag.color!; i < bbcode.length; i++) {\n          if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) {\n            bbcode[i].color = ctx.attr?.text;\n          }\n        }\n        break;\n      default:\n        KazumiLogger()\n            .e('BBCode: unrecognized Tag: ${ctx.text}, please submit an issue with logs, bangumi, and episode information');\n        break;\n    }\n  }\n\n  @override\n  void enterDocument(DocumentContext ctx) {}\n\n  @override\n  void exitDocument(DocumentContext ctx) {}\n\n  @override\n  void enterElement(ElementContext ctx) {}\n\n  @override\n  void exitElement(ElementContext ctx) {}\n\n  @override\n  void enterTag(TagContext ctx) {\n    _enterTag(ctx);\n  }\n\n  @override\n  void exitTag(TagContext ctx) {\n    _exitTag(ctx);\n  }\n\n  @override\n  void enterPlain(PlainContext ctx) {\n    bbcode.add(BBCodeText(text: ctx.text));\n  }\n\n  @override\n  void exitPlain(PlainContext ctx) {}\n\n  @override\n  void enterBgm(BgmContext ctx) {\n    /// 处理 (bgm35) 类型的表情\n    bbcode.add(BBCodeBgm(id: int.tryParse(ctx.id!.text!) ?? 0));\n  }\n\n  @override\n  void exitBgm(BgmContext ctx) {}\n\n  @override\n  void enterSticker(StickerContext ctx) {\n    /// 处理 (=A=) 类型的表情\n    /// ctx.start!.type 为 BBCode.tokens 内的 token 值\n    bbcode.add(BBCodeSticker(id: ctx.start!.type - 11));\n  }\n\n  @override\n  void exitSticker(StickerContext ctx) {}\n\n  @override\n  void enterEveryRule(ParserRuleContext ctx) {}\n\n  @override\n  void exitEveryRule(ParserRuleContext ctx) {}\n\n  @override\n  void visitTerminal(TerminalNode node) {}\n\n  @override\n  void visitErrorNode(ErrorNode node) {}\n}\n"
  },
  {
    "path": "lib/bbcode/bbcode_elements.dart",
    "content": "// 记录进入 tag 时 list 所在位置\nclass BBCodeTag {\n  int? bold;\n  int? italic;\n  int? underline;\n  int? strikeThrough;\n  int? masked;\n  int? quoted;\n  int? code;\n  int? size;\n  int? color;\n  int? link;\n  int? img;\n\n  void clear() {\n    bold = null;\n    italic = null;\n    underline = null;\n    strikeThrough = null;\n    masked = null;\n    quoted = null;\n    code = null;\n    size = null;\n    color = null;\n    link = null;\n    img = null;\n  }\n}\n\nclass BBCodeText {\n  String text;\n\n  bool bold = false;\n  bool italic = false;\n  bool underline = false;\n  bool strikeThrough = false;\n  bool masked = false;\n  bool quoted = false;\n  bool code = false;\n\n  int size = 14;\n  String? color;\n  String? link;\n\n  BBCodeText({\n    required this.text,\n    this.bold = false,\n    this.italic = false,\n    this.underline = false,\n    this.strikeThrough = false,\n    this.masked = false,\n    this.quoted = false,\n    this.code = false,\n    this.size = 14,\n    this.color,\n    this.link,\n  });\n}\n\nclass BBCodeBgm {\n  int id;\n\n  BBCodeBgm({required this.id});\n}\n\nclass BBCodeSticker {\n  int id;\n\n  BBCodeSticker({required this.id});\n}\n\nclass BBCodeImg {\n  String imageUrl;\n\n  BBCodeImg({required this.imageUrl});\n}\n\nBBCodeTag bbCodeTag = BBCodeTag();\n"
  },
  {
    "path": "lib/bbcode/bbcode_widget.dart",
    "content": "import 'dart:ui' as ui;\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:antlr4/antlr4.dart';\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport 'bbcode_base_listener.dart';\nimport 'bbcode_elements.dart';\nimport 'generated/BBCodeParser.dart';\nimport 'generated/BBCodeLexer.dart';\n\nclass BBCodeWidget extends StatefulWidget {\n  const BBCodeWidget({super.key, required this.bbcode});\n\n  final String bbcode;\n\n  @override\n  State<StatefulWidget> createState() => _BBCodeWidgetState();\n}\n\nclass _BBCodeWidgetState extends State<BBCodeWidget> {\n  bool _isVisible = false;\n\n  /// color 可以为三种表现形式\n  ///\n  /// `ARGB: #FFFFFFFF`\n  ///\n  /// `RGB: #FFFFFF`\n  ///\n  /// `NAME: red`\n  ///\n  /// 若全部解析失败则返回 null 使用默认颜色\n  Color? _parseColor(String hex) {\n    if (hex.startsWith('#')) {\n      hex = hex.replaceFirst('#', '');\n      if (hex.length == 6) {\n        hex = \"FF$hex\";\n      }\n      if (hex.length == 8) {\n        return Color(int.parse(hex, radix: 16));\n      }\n    }\n    switch (hex) {\n      case 'red':\n        return Colors.red;\n      case 'blue':\n        return Colors.blue;\n      case 'orange':\n        return Colors.orange;\n      case 'green':\n        return Colors.green;\n      case 'grey':\n        return Colors.grey;\n      default:\n        return null;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    BBCodeParser.checkVersion();\n    BBCodeParser.checkVersion();\n    final input = InputStream.fromString(widget.bbcode);\n    final lexer = BBCodeLexer(input);\n    final tokens = CommonTokenStream(lexer);\n    final parser = BBCodeParser(tokens);\n    final tree = parser.document();\n    final bbcodeBaseListener = BBCodeBaseListener();\n    ParseTreeWalker.DEFAULT.walk(bbcodeBaseListener, tree);\n    bbCodeTag.clear();\n\n    return Wrap(\n      children: [\n        SelectableText.rich(\n          TextSpan(\n            children: bbcodeBaseListener.bbcode.map((e) {\n              if (e is BBCodeText) {\n                Color? textColor = (!_isVisible && e.masked)\n                    ? Colors.transparent\n                    : (e.link != null)\n                        ? Colors.blue\n                        : (e.quoted)\n                            ? Theme.of(context).colorScheme.outline\n                            : (e.color != null)\n                                ? _parseColor(e.color!)\n                                : null;\n                return TextSpan(\n                  text: e.text,\n                  mouseCursor: (e.link != null || e.masked)\n                      ? SystemMouseCursors.click\n                      : SystemMouseCursors.text,\n                  recognizer: TapGestureRecognizer()\n                    ..onTap = (e.link != null || e.masked)\n                        ? () {\n                            if ((!e.masked || _isVisible) && e.link != null) {\n                              launchUrl(Uri.parse(e.link!));\n                            } else if (e.masked) {\n                              setState(() {\n                                _isVisible = !_isVisible;\n                              });\n                            }\n                          }\n                        : null,\n                  style: TextStyle(\n                    fontWeight: (e.bold) ? FontWeight.bold : null,\n                    fontStyle: (e.italic) ? FontStyle.italic : null,\n                    decoration: TextDecoration.combine([\n                      if (e.underline || e.link != null)\n                        TextDecoration.underline,\n                      if (e.strikeThrough) TextDecoration.lineThrough,\n                    ]),\n                    decorationColor: textColor,\n                    fontSize: e.size.toDouble(),\n                    color: textColor,\n                    backgroundColor:\n                        (!_isVisible && e.masked) ? Color(0xFF555555) : null,\n                    fontFeatures: [FontFeature.tabularFigures()],\n                  ),\n                );\n              } else if (e is BBCodeImg) {\n                return WidgetSpan(\n                  child: CachedNetworkImage(\n                    imageUrl: e.imageUrl,\n                    placeholder: (context, url) =>\n                        const SizedBox(width: 1, height: 1),\n                    errorWidget: (context, error, stackTrace) {\n                      return const Text('.');\n                    },\n                  ),\n                );\n              } else if (e is BBCodeBgm) {\n                String url;\n                if (e.id == 11 || e.id == 23) {\n                  url = 'https://bangumi.tv/img/smiles/bgm/${e.id}.gif';\n                }\n                if (e.id < 24) {\n                  url = 'https://bangumi.tv/img/smiles/bgm/${e.id}.png';\n                }\n                if (e.id < 33) {\n                  url = 'https://bangumi.tv/img/smiles/tv/0${e.id - 23}.gif';\n                }\n                url = 'https://bangumi.tv/img/smiles/tv/${e.id - 23}.gif';\n                return WidgetSpan(\n                  child: CachedNetworkImage(\n                    imageUrl: url,\n                    placeholder: (context, url) =>\n                        const SizedBox(width: 1, height: 1),\n                    errorWidget: (context, error, stackTrace) {\n                      return const Text('.');\n                    },\n                  ),\n                );\n              } else if (e is BBCodeSticker) {\n                return WidgetSpan(\n                  child: CachedNetworkImage(\n                    imageUrl: 'https://bangumi.tv/img/smiles/${e.id}.gif',\n                    placeholder: (context, url) =>\n                        const SizedBox(width: 1, height: 1),\n                    errorWidget: (context, error, stackTrace) {\n                      return const Text('.');\n                    },\n                  ),\n                );\n              } else {\n                // e is Icon\n                return WidgetSpan(\n                  child: Icon(\n                    (e as Icon).icon,\n                    color: Theme.of(context).colorScheme.outline,\n                  ),\n                  alignment: PlaceholderAlignment.top,\n                );\n              }\n            }).toList(),\n          ),\n          selectionHeightStyle: ui.BoxHeightStyle.max,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bbcode/generated/BBCode.tokens",
    "content": "T__0=1\nT__1=2\nT__2=3\nT__3=4\nT__4=5\nT__5=6\nT__6=7\nT__7=8\nT__8=9\nT__9=10\nT__10=11\nT__11=12\nT__12=13\nT__13=14\nT__14=15\nT__15=16\nT__16=17\nT__17=18\nT__18=19\nT__19=20\nT__20=21\nT__21=22\nT__22=23\nT__23=24\nT__24=25\nT__25=26\nT__26=27\nSTRING=28\n'['=1\n'='=2\n']'=3\n'[/'=4\n'/'=5\n'('=6\n')'=7\n'[来自Bangumi for android]'=8\n'[来自Bangumi for iOS]'=9\n'(bgm'=10\n'(BGM'=11\n'(=A=)'=12\n'(=w=)'=13\n'(-w=)'=14\n'(S_S)'=15\n'(=v=)'=16\n'(@_@)'=17\n'(=W=)'=18\n'(TAT)'=19\n'(T_T)'=20\n'(=\\'=)'=21\n'(=3=)'=22\n'(= =\\')'=23\n'(=///=)'=24\n'(=.,=)'=25\n'(:P)'=26\n'(LOL)'=27\n"
  },
  {
    "path": "lib/bbcode/generated/BBCodeLexer.dart",
    "content": "import 'package:antlr4/antlr4.dart';\n\n\nclass BBCodeLexer extends Lexer {\n  static final checkVersion = () => RuntimeMetaData.checkVersion('4.13.2', RuntimeMetaData.VERSION);\n\n  static final List<DFA> _decisionToDFA = List.generate(\n        _ATN.numberOfDecisions, (i) => DFA(_ATN.getDecisionState(i), i));\n  static final PredictionContextCache _sharedContextCache = PredictionContextCache();\n  static const int\n    TOKEN_T__0 = 1, TOKEN_T__1 = 2, TOKEN_T__2 = 3, TOKEN_T__3 = 4, TOKEN_T__4 = 5, \n    TOKEN_T__5 = 6, TOKEN_T__6 = 7, TOKEN_T__7 = 8, TOKEN_T__8 = 9, TOKEN_T__9 = 10, \n    TOKEN_T__10 = 11, TOKEN_T__11 = 12, TOKEN_T__12 = 13, TOKEN_T__13 = 14, \n    TOKEN_T__14 = 15, TOKEN_T__15 = 16, TOKEN_T__16 = 17, TOKEN_T__17 = 18, \n    TOKEN_T__18 = 19, TOKEN_T__19 = 20, TOKEN_T__20 = 21, TOKEN_T__21 = 22, \n    TOKEN_T__22 = 23, TOKEN_T__23 = 24, TOKEN_T__24 = 25, TOKEN_T__25 = 26, \n    TOKEN_T__26 = 27, TOKEN_STRING = 28;\n  @override\n  final List<String> channelNames = [\n    'DEFAULT_TOKEN_CHANNEL', 'HIDDEN'\n  ];\n\n  @override\n  final List<String> modeNames = [\n    'DEFAULT_MODE'\n  ];\n\n  @override\n  final List<String> ruleNames = [\n    'T__0', 'T__1', 'T__2', 'T__3', 'T__4', 'T__5', 'T__6', 'T__7', 'T__8', \n    'T__9', 'T__10', 'T__11', 'T__12', 'T__13', 'T__14', 'T__15', 'T__16', \n    'T__17', 'T__18', 'T__19', 'T__20', 'T__21', 'T__22', 'T__23', 'T__24', \n    'T__25', 'T__26', 'STRING'\n  ];\n\n  static final List<String?> _LITERAL_NAMES = [\n      null, \"'['\", \"'='\", \"']'\", \"'[/'\", \"'/'\", \"'('\", \"')'\", \"'[\\\\u6765\\\\u81EABangumi for android]'\", \n      \"'[\\\\u6765\\\\u81EABangumi for iOS]'\", \"'(bgm'\", \"'(BGM'\", \"'(=A=)'\", \n      \"'(=w=)'\", \"'(-w=)'\", \"'(S_S)'\", \"'(=v=)'\", \"'(@_@)'\", \"'(=W=)'\", \n      \"'(TAT)'\", \"'(T_T)'\", \"'(='=)'\", \"'(=3=)'\", \"'(= =')'\", \"'(=///=)'\", \n      \"'(=.,=)'\", \"'(:P)'\", \"'(LOL)'\"\n  ];\n  static final List<String?> _SYMBOLIC_NAMES = [\n      null, null, null, null, null, null, null, null, null, null, null, \n      null, null, null, null, null, null, null, null, null, null, null, \n      null, null, null, null, null, null, \"STRING\"\n  ];\n  static final Vocabulary VOCABULARY = VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);\n\n  @override\n  Vocabulary get vocabulary {\n    return VOCABULARY;\n  }\n\n\n  BBCodeLexer(CharStream input) : super(input) {\n    interpreter = LexerATNSimulator(_ATN, _decisionToDFA, _sharedContextCache, recog: this);\n  }\n\n  @override\n  List<int> get serializedATN => _serializedATN;\n\n  @override\n  String get grammarFileName => 'BBCode.g4';\n\n  @override\n  ATN getATN() { return _ATN; }\n\n  static const List<int> _serializedATN = [\n      4,0,28,230,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,\n      6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,\n      13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,\n      7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,\n      27,7,27,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,\n      6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,\n      1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,\n      8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,\n      1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,\n      12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,\n      1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,\n      16,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,19,\n      1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,20,1,21,1,21,1,\n      21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,\n      1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,25,1,\n      25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,27,4,27,227,8,27,\n      11,27,12,27,228,0,0,28,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,\n      10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,\n      21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,1,0,1,4,0,40,41,61,61,\n      91,91,93,93,230,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,\n      9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,\n      1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,\n      0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,\n      0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,\n      0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,1,57,1,0,0,0,3,59,1,0,0,0,\n      5,61,1,0,0,0,7,63,1,0,0,0,9,66,1,0,0,0,11,68,1,0,0,0,13,70,1,0,0,0,\n      15,72,1,0,0,0,17,96,1,0,0,0,19,116,1,0,0,0,21,121,1,0,0,0,23,126,1,\n      0,0,0,25,132,1,0,0,0,27,138,1,0,0,0,29,144,1,0,0,0,31,150,1,0,0,0,\n      33,156,1,0,0,0,35,162,1,0,0,0,37,168,1,0,0,0,39,174,1,0,0,0,41,180,\n      1,0,0,0,43,186,1,0,0,0,45,192,1,0,0,0,47,199,1,0,0,0,49,207,1,0,0,\n      0,51,214,1,0,0,0,53,219,1,0,0,0,55,226,1,0,0,0,57,58,5,91,0,0,58,2,\n      1,0,0,0,59,60,5,61,0,0,60,4,1,0,0,0,61,62,5,93,0,0,62,6,1,0,0,0,63,\n      64,5,91,0,0,64,65,5,47,0,0,65,8,1,0,0,0,66,67,5,47,0,0,67,10,1,0,0,\n      0,68,69,5,40,0,0,69,12,1,0,0,0,70,71,5,41,0,0,71,14,1,0,0,0,72,73,\n      5,91,0,0,73,74,5,26469,0,0,74,75,5,33258,0,0,75,76,5,66,0,0,76,77,\n      5,97,0,0,77,78,5,110,0,0,78,79,5,103,0,0,79,80,5,117,0,0,80,81,5,109,\n      0,0,81,82,5,105,0,0,82,83,5,32,0,0,83,84,5,102,0,0,84,85,5,111,0,0,\n      85,86,5,114,0,0,86,87,5,32,0,0,87,88,5,97,0,0,88,89,5,110,0,0,89,90,\n      5,100,0,0,90,91,5,114,0,0,91,92,5,111,0,0,92,93,5,105,0,0,93,94,5,\n      100,0,0,94,95,5,93,0,0,95,16,1,0,0,0,96,97,5,91,0,0,97,98,5,26469,\n      0,0,98,99,5,33258,0,0,99,100,5,66,0,0,100,101,5,97,0,0,101,102,5,110,\n      0,0,102,103,5,103,0,0,103,104,5,117,0,0,104,105,5,109,0,0,105,106,\n      5,105,0,0,106,107,5,32,0,0,107,108,5,102,0,0,108,109,5,111,0,0,109,\n      110,5,114,0,0,110,111,5,32,0,0,111,112,5,105,0,0,112,113,5,79,0,0,\n      113,114,5,83,0,0,114,115,5,93,0,0,115,18,1,0,0,0,116,117,5,40,0,0,\n      117,118,5,98,0,0,118,119,5,103,0,0,119,120,5,109,0,0,120,20,1,0,0,\n      0,121,122,5,40,0,0,122,123,5,66,0,0,123,124,5,71,0,0,124,125,5,77,\n      0,0,125,22,1,0,0,0,126,127,5,40,0,0,127,128,5,61,0,0,128,129,5,65,\n      0,0,129,130,5,61,0,0,130,131,5,41,0,0,131,24,1,0,0,0,132,133,5,40,\n      0,0,133,134,5,61,0,0,134,135,5,119,0,0,135,136,5,61,0,0,136,137,5,\n      41,0,0,137,26,1,0,0,0,138,139,5,40,0,0,139,140,5,45,0,0,140,141,5,\n      119,0,0,141,142,5,61,0,0,142,143,5,41,0,0,143,28,1,0,0,0,144,145,5,\n      40,0,0,145,146,5,83,0,0,146,147,5,95,0,0,147,148,5,83,0,0,148,149,\n      5,41,0,0,149,30,1,0,0,0,150,151,5,40,0,0,151,152,5,61,0,0,152,153,\n      5,118,0,0,153,154,5,61,0,0,154,155,5,41,0,0,155,32,1,0,0,0,156,157,\n      5,40,0,0,157,158,5,64,0,0,158,159,5,95,0,0,159,160,5,64,0,0,160,161,\n      5,41,0,0,161,34,1,0,0,0,162,163,5,40,0,0,163,164,5,61,0,0,164,165,\n      5,87,0,0,165,166,5,61,0,0,166,167,5,41,0,0,167,36,1,0,0,0,168,169,\n      5,40,0,0,169,170,5,84,0,0,170,171,5,65,0,0,171,172,5,84,0,0,172,173,\n      5,41,0,0,173,38,1,0,0,0,174,175,5,40,0,0,175,176,5,84,0,0,176,177,\n      5,95,0,0,177,178,5,84,0,0,178,179,5,41,0,0,179,40,1,0,0,0,180,181,\n      5,40,0,0,181,182,5,61,0,0,182,183,5,39,0,0,183,184,5,61,0,0,184,185,\n      5,41,0,0,185,42,1,0,0,0,186,187,5,40,0,0,187,188,5,61,0,0,188,189,\n      5,51,0,0,189,190,5,61,0,0,190,191,5,41,0,0,191,44,1,0,0,0,192,193,\n      5,40,0,0,193,194,5,61,0,0,194,195,5,32,0,0,195,196,5,61,0,0,196,197,\n      5,39,0,0,197,198,5,41,0,0,198,46,1,0,0,0,199,200,5,40,0,0,200,201,\n      5,61,0,0,201,202,5,47,0,0,202,203,5,47,0,0,203,204,5,47,0,0,204,205,\n      5,61,0,0,205,206,5,41,0,0,206,48,1,0,0,0,207,208,5,40,0,0,208,209,\n      5,61,0,0,209,210,5,46,0,0,210,211,5,44,0,0,211,212,5,61,0,0,212,213,\n      5,41,0,0,213,50,1,0,0,0,214,215,5,40,0,0,215,216,5,58,0,0,216,217,\n      5,80,0,0,217,218,5,41,0,0,218,52,1,0,0,0,219,220,5,40,0,0,220,221,\n      5,76,0,0,221,222,5,79,0,0,222,223,5,76,0,0,223,224,5,41,0,0,224,54,\n      1,0,0,0,225,227,8,0,0,0,226,225,1,0,0,0,227,228,1,0,0,0,228,226,1,\n      0,0,0,228,229,1,0,0,0,229,56,1,0,0,0,2,0,228,0\n  ];\n\n  static final ATN _ATN =\n      ATNDeserializer().deserialize(_serializedATN);\n}"
  },
  {
    "path": "lib/bbcode/generated/BBCodeListener.dart",
    "content": "import 'package:antlr4/antlr4.dart';\n\nimport 'BBCodeParser.dart';\n\n/// This abstract class defines a complete listener for a parse tree produced by\n/// [BBCodeParser].\nabstract class BBCodeListener extends ParseTreeListener {\n  /// Enter a parse tree produced by [BBCodeParser.document].\n  /// [ctx] the parse tree\n  void enterDocument(DocumentContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.document].\n  /// [ctx] the parse tree\n  void exitDocument(DocumentContext ctx);\n\n  /// Enter a parse tree produced by [BBCodeParser.element].\n  /// [ctx] the parse tree\n  void enterElement(ElementContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.element].\n  /// [ctx] the parse tree\n  void exitElement(ElementContext ctx);\n\n  /// Enter a parse tree produced by [BBCodeParser.tag].\n  /// [ctx] the parse tree\n  void enterTag(TagContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.tag].\n  /// [ctx] the parse tree\n  void exitTag(TagContext ctx);\n\n  /// Enter a parse tree produced by [BBCodeParser.plain].\n  /// [ctx] the parse tree\n  void enterPlain(PlainContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.plain].\n  /// [ctx] the parse tree\n  void exitPlain(PlainContext ctx);\n\n  /// Enter a parse tree produced by [BBCodeParser.bgm].\n  /// [ctx] the parse tree\n  void enterBgm(BgmContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.bgm].\n  /// [ctx] the parse tree\n  void exitBgm(BgmContext ctx);\n\n  /// Enter a parse tree produced by [BBCodeParser.sticker].\n  /// [ctx] the parse tree\n  void enterSticker(StickerContext ctx);\n  /// Exit a parse tree produced by [BBCodeParser.sticker].\n  /// [ctx] the parse tree\n  void exitSticker(StickerContext ctx);\n}"
  },
  {
    "path": "lib/bbcode/generated/BBCodeParser.dart",
    "content": "import 'package:antlr4/antlr4.dart';\n\nimport 'BBCodeListener.dart';\nconst int RULE_document = 0, RULE_element = 1, RULE_tag = 2, RULE_plain = 3, \n          RULE_bgm = 4, RULE_sticker = 5;\nclass BBCodeParser extends Parser {\n  static final checkVersion = () => RuntimeMetaData.checkVersion('4.13.2', RuntimeMetaData.VERSION);\n  static const int TOKEN_EOF = IntStream.EOF;\n\n  static final List<DFA> _decisionToDFA = List.generate(\n      _ATN.numberOfDecisions, (i) => DFA(_ATN.getDecisionState(i), i));\n  static final PredictionContextCache _sharedContextCache = PredictionContextCache();\n  static const int TOKEN_T__0 = 1, TOKEN_T__1 = 2, TOKEN_T__2 = 3, TOKEN_T__3 = 4, \n                   TOKEN_T__4 = 5, TOKEN_T__5 = 6, TOKEN_T__6 = 7, TOKEN_T__7 = 8, \n                   TOKEN_T__8 = 9, TOKEN_T__9 = 10, TOKEN_T__10 = 11, TOKEN_T__11 = 12, \n                   TOKEN_T__12 = 13, TOKEN_T__13 = 14, TOKEN_T__14 = 15, \n                   TOKEN_T__15 = 16, TOKEN_T__16 = 17, TOKEN_T__17 = 18, \n                   TOKEN_T__18 = 19, TOKEN_T__19 = 20, TOKEN_T__20 = 21, \n                   TOKEN_T__21 = 22, TOKEN_T__22 = 23, TOKEN_T__23 = 24, \n                   TOKEN_T__24 = 25, TOKEN_T__25 = 26, TOKEN_T__26 = 27, \n                   TOKEN_STRING = 28;\n\n  @override\n  final List<String> ruleNames = [\n    'document', 'element', 'tag', 'plain', 'bgm', 'sticker'\n  ];\n\n  static final List<String?> _LITERAL_NAMES = [\n      null, \"'['\", \"'='\", \"']'\", \"'[/'\", \"'/'\", \"'('\", \"')'\", \"'[\\\\u6765\\\\u81EABangumi for android]'\", \n      \"'[\\\\u6765\\\\u81EABangumi for iOS]'\", \"'(bgm'\", \"'(BGM'\", \"'(=A=)'\", \n      \"'(=w=)'\", \"'(-w=)'\", \"'(S_S)'\", \"'(=v=)'\", \"'(@_@)'\", \"'(=W=)'\", \n      \"'(TAT)'\", \"'(T_T)'\", \"'(='=)'\", \"'(=3=)'\", \"'(= =')'\", \"'(=///=)'\", \n      \"'(=.,=)'\", \"'(:P)'\", \"'(LOL)'\"\n  ];\n  static final List<String?> _SYMBOLIC_NAMES = [\n      null, null, null, null, null, null, null, null, null, null, null, \n      null, null, null, null, null, null, null, null, null, null, null, \n      null, null, null, null, null, null, \"STRING\"\n  ];\n  static final Vocabulary VOCABULARY = VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);\n\n  @override\n  Vocabulary get vocabulary {\n    return VOCABULARY;\n  }\n\n  @override\n  String get grammarFileName => 'BBCode.g4';\n\n  @override\n  List<int> get serializedATN => _serializedATN;\n\n  @override\n  ATN getATN() {\n   return _ATN;\n  }\n\n  BBCodeParser(TokenStream input) : super(input) {\n    interpreter = ParserATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache);\n  }\n\n  DocumentContext document() {\n    dynamic _localctx = DocumentContext(context, state);\n    enterRule(_localctx, 0, RULE_document);\n    int _la;\n    try {\n      enterOuterAlt(_localctx, 1);\n      state = 15;\n      errorHandler.sync(this);\n      _la = tokenStream.LA(1)!;\n      while ((((_la) & ~0x3f) == 0 && ((1 << _la) & 536870894) != 0)) {\n        state = 12;\n        element();\n        state = 17;\n        errorHandler.sync(this);\n        _la = tokenStream.LA(1)!;\n      }\n      state = 18;\n      match(TOKEN_EOF);\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  ElementContext element() {\n    dynamic _localctx = ElementContext(context, state);\n    enterRule(_localctx, 2, RULE_element);\n    try {\n      state = 24;\n      errorHandler.sync(this);\n      switch (interpreter!.adaptivePredict(tokenStream, 1, context)) {\n      case 1:\n        enterOuterAlt(_localctx, 1);\n        state = 20;\n        tag();\n        break;\n      case 2:\n        enterOuterAlt(_localctx, 2);\n        state = 21;\n        plain();\n        break;\n      case 3:\n        enterOuterAlt(_localctx, 3);\n        state = 22;\n        bgm();\n        break;\n      case 4:\n        enterOuterAlt(_localctx, 4);\n        state = 23;\n        sticker();\n        break;\n      }\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  TagContext tag() {\n    dynamic _localctx = TagContext(context, state);\n    enterRule(_localctx, 4, RULE_tag);\n    int _la;\n    try {\n      enterOuterAlt(_localctx, 1);\n      state = 26;\n      match(TOKEN_T__0);\n      state = 27;\n      _localctx.tagName = match(TOKEN_STRING);\n      state = 30;\n      errorHandler.sync(this);\n      _la = tokenStream.LA(1)!;\n      if (_la == TOKEN_T__1) {\n        state = 28;\n        match(TOKEN_T__1);\n        state = 29;\n        _localctx.attr = match(TOKEN_STRING);\n      }\n\n      state = 32;\n      match(TOKEN_T__2);\n      state = 36;\n      errorHandler.sync(this);\n      _la = tokenStream.LA(1)!;\n      while ((((_la) & ~0x3f) == 0 && ((1 << _la) & 536870894) != 0)) {\n        state = 33;\n        _localctx.content = element();\n        state = 38;\n        errorHandler.sync(this);\n        _la = tokenStream.LA(1)!;\n      }\n      state = 39;\n      match(TOKEN_T__3);\n      state = 40;\n      match(TOKEN_STRING);\n      state = 41;\n      match(TOKEN_T__2);\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  PlainContext plain() {\n    dynamic _localctx = PlainContext(context, state);\n    enterRule(_localctx, 6, RULE_plain);\n    int _la;\n    try {\n      int _alt;\n      state = 50;\n      errorHandler.sync(this);\n      switch (tokenStream.LA(1)!) {\n      case TOKEN_T__0:\n      case TOKEN_T__1:\n      case TOKEN_T__2:\n      case TOKEN_T__4:\n      case TOKEN_T__5:\n      case TOKEN_T__6:\n      case TOKEN_STRING:\n        enterOuterAlt(_localctx, 1);\n        state = 44; \n        errorHandler.sync(this);\n        _alt = 1;\n        do {\n          switch (_alt) {\n          case 1:\n            state = 43;\n            _la = tokenStream.LA(1)!;\n            if (!((((_la) & ~0x3f) == 0 && ((1 << _la) & 268435694) != 0))) {\n            errorHandler.recoverInline(this);\n            } else {\n              if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true;\n              errorHandler.reportMatch(this);\n              consume();\n            }\n            break;\n          default:\n            throw NoViableAltException(this);\n          }\n          state = 46; \n          errorHandler.sync(this);\n          _alt = interpreter!.adaptivePredict(tokenStream, 4, context);\n        } while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER);\n        break;\n      case TOKEN_T__7:\n        enterOuterAlt(_localctx, 2);\n        state = 48;\n        match(TOKEN_T__7);\n        break;\n      case TOKEN_T__8:\n        enterOuterAlt(_localctx, 3);\n        state = 49;\n        match(TOKEN_T__8);\n        break;\n      default:\n        throw NoViableAltException(this);\n      }\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  BgmContext bgm() {\n    dynamic _localctx = BgmContext(context, state);\n    enterRule(_localctx, 8, RULE_bgm);\n    int _la;\n    try {\n      enterOuterAlt(_localctx, 1);\n      state = 52;\n      _la = tokenStream.LA(1)!;\n      if (!(_la == TOKEN_T__9 || _la == TOKEN_T__10)) {\n      errorHandler.recoverInline(this);\n      } else {\n        if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true;\n        errorHandler.reportMatch(this);\n        consume();\n      }\n      state = 53;\n      _localctx.id = match(TOKEN_STRING);\n      state = 54;\n      match(TOKEN_T__6);\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  StickerContext sticker() {\n    dynamic _localctx = StickerContext(context, state);\n    enterRule(_localctx, 10, RULE_sticker);\n    int _la;\n    try {\n      enterOuterAlt(_localctx, 1);\n      state = 56;\n      _la = tokenStream.LA(1)!;\n      if (!((((_la) & ~0x3f) == 0 && ((1 << _la) & 268431360) != 0))) {\n      errorHandler.recoverInline(this);\n      } else {\n        if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true;\n        errorHandler.reportMatch(this);\n        consume();\n      }\n    } on RecognitionException catch (re) {\n      _localctx.exception = re;\n      errorHandler.reportError(this, re);\n      errorHandler.recover(this, re);\n    } finally {\n      exitRule();\n    }\n    return _localctx;\n  }\n\n  static const List<int> _serializedATN = [\n      4,1,28,59,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,1,0,5,0,\n      14,8,0,10,0,12,0,17,9,0,1,0,1,0,1,1,1,1,1,1,1,1,3,1,25,8,1,1,2,1,2,\n      1,2,1,2,3,2,31,8,2,1,2,1,2,5,2,35,8,2,10,2,12,2,38,9,2,1,2,1,2,1,2,\n      1,2,1,3,4,3,45,8,3,11,3,12,3,46,1,3,1,3,3,3,51,8,3,1,4,1,4,1,4,1,4,\n      1,5,1,5,1,5,0,0,6,0,2,4,6,8,10,0,3,3,0,1,3,5,7,28,28,1,0,10,11,1,0,\n      12,27,61,0,15,1,0,0,0,2,24,1,0,0,0,4,26,1,0,0,0,6,50,1,0,0,0,8,52,\n      1,0,0,0,10,56,1,0,0,0,12,14,3,2,1,0,13,12,1,0,0,0,14,17,1,0,0,0,15,\n      13,1,0,0,0,15,16,1,0,0,0,16,18,1,0,0,0,17,15,1,0,0,0,18,19,5,0,0,1,\n      19,1,1,0,0,0,20,25,3,4,2,0,21,25,3,6,3,0,22,25,3,8,4,0,23,25,3,10,\n      5,0,24,20,1,0,0,0,24,21,1,0,0,0,24,22,1,0,0,0,24,23,1,0,0,0,25,3,1,\n      0,0,0,26,27,5,1,0,0,27,30,5,28,0,0,28,29,5,2,0,0,29,31,5,28,0,0,30,\n      28,1,0,0,0,30,31,1,0,0,0,31,32,1,0,0,0,32,36,5,3,0,0,33,35,3,2,1,0,\n      34,33,1,0,0,0,35,38,1,0,0,0,36,34,1,0,0,0,36,37,1,0,0,0,37,39,1,0,\n      0,0,38,36,1,0,0,0,39,40,5,4,0,0,40,41,5,28,0,0,41,42,5,3,0,0,42,5,\n      1,0,0,0,43,45,7,0,0,0,44,43,1,0,0,0,45,46,1,0,0,0,46,44,1,0,0,0,46,\n      47,1,0,0,0,47,51,1,0,0,0,48,51,5,8,0,0,49,51,5,9,0,0,50,44,1,0,0,0,\n      50,48,1,0,0,0,50,49,1,0,0,0,51,7,1,0,0,0,52,53,7,1,0,0,53,54,5,28,\n      0,0,54,55,5,7,0,0,55,9,1,0,0,0,56,57,7,2,0,0,57,11,1,0,0,0,6,15,24,\n      30,36,46,50\n  ];\n\n  static final ATN _ATN =\n      ATNDeserializer().deserialize(_serializedATN);\n}\nclass DocumentContext extends ParserRuleContext {\n  TerminalNode? EOF() => getToken(BBCodeParser.TOKEN_EOF, 0);\n  List<ElementContext> elements() => getRuleContexts<ElementContext>();\n  ElementContext? element(int i) => getRuleContext<ElementContext>(i);\n  DocumentContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_document;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterDocument(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitDocument(this);\n  }\n}\n\nclass ElementContext extends ParserRuleContext {\n  TagContext? tag() => getRuleContext<TagContext>(0);\n  PlainContext? plain() => getRuleContext<PlainContext>(0);\n  BgmContext? bgm() => getRuleContext<BgmContext>(0);\n  StickerContext? sticker() => getRuleContext<StickerContext>(0);\n  ElementContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_element;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterElement(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitElement(this);\n  }\n}\n\nclass TagContext extends ParserRuleContext {\n  Token? tagName;\n  Token? attr;\n  ElementContext? content;\n  List<TerminalNode> STRINGs() => getTokens(BBCodeParser.TOKEN_STRING);\n  TerminalNode? STRING(int i) => getToken(BBCodeParser.TOKEN_STRING, i);\n  List<ElementContext> elements() => getRuleContexts<ElementContext>();\n  ElementContext? element(int i) => getRuleContext<ElementContext>(i);\n  TagContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_tag;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterTag(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitTag(this);\n  }\n}\n\nclass PlainContext extends ParserRuleContext {\n  List<TerminalNode> STRINGs() => getTokens(BBCodeParser.TOKEN_STRING);\n  TerminalNode? STRING(int i) => getToken(BBCodeParser.TOKEN_STRING, i);\n  PlainContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_plain;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterPlain(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitPlain(this);\n  }\n}\n\nclass BgmContext extends ParserRuleContext {\n  Token? id;\n  TerminalNode? STRING() => getToken(BBCodeParser.TOKEN_STRING, 0);\n  BgmContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_bgm;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterBgm(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitBgm(this);\n  }\n}\n\nclass StickerContext extends ParserRuleContext {\n  StickerContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState);\n  @override\n  int get ruleIndex => RULE_sticker;\n  @override\n  void enterRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.enterSticker(this);\n  }\n  @override\n  void exitRule(ParseTreeListener listener) {\n    if (listener is BBCodeListener) listener.exitSticker(this);\n  }\n}\n\n"
  },
  {
    "path": "lib/bean/appbar/drag_to_move_bar.dart",
    "content": "import 'package:kazumi/utils/utils.dart';\nimport 'package:flutter/material.dart';\nimport 'package:window_manager/window_manager.dart';\n\n/// A widget for drag to move window.\n///\n/// When you have hidden the title bar, you can add this widget to move the window position.\n///\n/// {@tool snippet}\n///\n/// The sample creates a red box, drag the box to move the window.\n///\n/// ```dart\n/// DragToMoveArea(\n///   child: Container(\n///     width: 300,\n///     height: 32,\n///     color: Colors.red,\n///   ),\n/// )\n/// ```\n/// {@end-tool}\nclass DragToMoveArea extends StatelessWidget {\n  const DragToMoveArea({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onPanStart: (_) => (Utils.isDesktop()) ? windowManager.startDragging() : null,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/appbar/safe_mediaquery_warpper.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// workaround for padding check error on Xiaomi HyperOS devices\n/// caused by flutter/flutter#161086\n/// this is a temporary solution, will be removed in the future\nclass SafeMediaQueryWrapper extends StatelessWidget {\n  final Widget child;\n  final double defaultTopPadding;\n  final double defaultBottomPadding;\n\n  const SafeMediaQueryWrapper({\n    super.key,\n    required this.child,\n    this.defaultTopPadding = 25,\n    this.defaultBottomPadding = 0,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final mediaQuery = MediaQuery.of(context);\n    final viewPadding = mediaQuery.viewPadding;\n\n    final isPaddingCheckError = viewPadding.top < 0 || viewPadding.top > 50;\n\n    if (!isPaddingCheckError) {\n      return child;\n    }\n\n    return MediaQuery(\n      data: mediaQuery.copyWith(\n        viewPadding: EdgeInsets.only(\n          top: defaultTopPadding,\n          bottom: defaultBottomPadding,\n          left: viewPadding.left,\n          right: viewPadding.right,\n        ),\n        padding: EdgeInsets.only(\n          top: defaultTopPadding,\n          bottom: defaultBottomPadding,\n          left: mediaQuery.padding.left,\n          right: mediaQuery.padding.right,\n        ),\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/appbar/sys_app_bar.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass SysAppBar extends StatelessWidget implements PreferredSizeWidget {\n  final double? toolbarHeight;\n\n  final Widget? title;\n\n  final Color? backgroundColor;\n\n  final double? elevation;\n\n  final ShapeBorder? shape;\n\n  final List<Widget>? actions;\n\n  final Widget? leading;\n\n  final double? leadingWidth;\n\n  final PreferredSizeWidget? bottom;\n\n  final bool needTopOffset;\n\n  const SysAppBar(\n      {super.key,\n      this.toolbarHeight,\n      this.title,\n      this.backgroundColor,\n      this.elevation,\n      this.shape,\n      this.actions,\n      this.leading,\n      this.leadingWidth,\n      this.bottom,\n      this.needTopOffset = true});\n\n  bool showWindowButton() {\n    return GStorage.setting\n        .get(SettingBoxKey.showWindowButton, defaultValue: false);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> acs = [];\n    if (actions != null) {\n      acs.addAll(actions!);\n    }\n    if (Utils.isDesktop()) {\n      // acs.add(IconButton(onPressed: () => windowManager.minimize(), icon: const Icon(Icons.minimize)));\n      if (!showWindowButton()) {\n        acs.add(CloseButton(onPressed: () => windowManager.close()));\n      }\n      acs.add(const SizedBox(width: 8));\n    }\n    return GestureDetector(\n      onPanStart: (_) =>\n          (Utils.isDesktop()) ? windowManager.startDragging() : null,\n      child: AppBar(\n        toolbarHeight: preferredSize.height,\n        scrolledUnderElevation: 0.0,\n        title: title != null\n            ? EmbeddedNativeControlArea(\n                requireOffset: needTopOffset,\n                child: title!,\n              )\n            : null,\n        centerTitle: Platform.isIOS ? true : false,\n        actions: acs.map((e) {\n          return EmbeddedNativeControlArea(\n            requireOffset: needTopOffset,\n            child: e,\n          );\n        }).toList(),\n        leading: leading != null\n            ? EmbeddedNativeControlArea(\n                requireOffset: needTopOffset,\n                child: leading!,\n              )\n            : Navigator.canPop(context)\n                ? EmbeddedNativeControlArea(\n                    requireOffset: needTopOffset,\n                    child: IconButton(\n                      onPressed: () {\n                        Navigator.maybePop(context);\n                      },\n                      icon: Icon(Icons.arrow_back),\n                    ),\n                  )\n                : null,\n        leadingWidth: leadingWidth,\n        backgroundColor: backgroundColor,\n        elevation: elevation,\n        shape: shape,\n        bottom: bottom,\n        automaticallyImplyLeading: false,\n        systemOverlayStyle: SystemUiOverlayStyle(\n          statusBarColor: Colors.transparent,\n          statusBarIconBrightness:\n              Theme.of(context).brightness == Brightness.light\n                  ? Brightness.dark\n                  : Brightness.light,\n          systemNavigationBarColor: Colors.transparent,\n          systemNavigationBarDividerColor: Colors.transparent,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Size get preferredSize {\n    // macOS needs to add 22(macOS title bar height)\n    // to default toolbar height to build appbar like normal\n    if (Platform.isMacOS && needTopOffset && showWindowButton()) {\n      if (toolbarHeight != null) {\n        return Size.fromHeight(toolbarHeight! + 22);\n      } else {\n        return const Size.fromHeight(kToolbarHeight + 22);\n      }\n    } else {\n      return Size.fromHeight(toolbarHeight ?? kToolbarHeight);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/bangumi_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/utils/utils.dart';\n\n// 视频卡片 - 垂直布局\nclass BangumiCardV extends StatelessWidget {\n  const BangumiCardV({\n    super.key,\n    required this.bangumiItem,\n    this.canTap = true,\n    this.enableHero = true,\n  });\n\n  final BangumiItem bangumiItem;\n  final bool canTap;\n  final bool enableHero;\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      elevation: 0,\n      clipBehavior: Clip.antiAlias,\n      margin: EdgeInsets.zero,\n      child: GestureDetector(\n        child: InkWell(\n          onTap: () {\n            if (!canTap) {\n              KazumiDialog.showToast(\n                message: '编辑模式',\n              );\n              return;\n            }\n            Modular.to.pushNamed('/info/', arguments: bangumiItem);\n          },\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              AspectRatio(\n                aspectRatio: 0.65,\n                child: LayoutBuilder(builder: (context, boxConstraints) {\n                  final double maxWidth = boxConstraints.maxWidth;\n                  final double maxHeight = boxConstraints.maxHeight;\n                  return enableHero\n                      ? Hero(\n                          transitionOnUserGestures: true,\n                          tag: bangumiItem.id,\n                          child: NetworkImgLayer(\n                            src: bangumiItem.images['large'] ?? '',\n                            width: maxWidth,\n                            height: maxHeight,\n                          ),\n                        )\n                      : NetworkImgLayer(\n                          src: bangumiItem.images['large'] ?? '',\n                          width: maxWidth,\n                          height: maxHeight,\n                        );\n                }),\n              ),\n              BangumiContent(bangumiItem: bangumiItem)\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass BangumiContent extends StatelessWidget {\n  const BangumiContent({super.key, required this.bangumiItem});\n\n  final BangumiItem bangumiItem;\n\n  @override\n  Widget build(BuildContext context) {\n    final ts = MediaQuery.textScalerOf(context);\n\n    final int maxTextLines = Utils.isDesktop() ? 3 \n      : (Utils.isTablet() && MediaQuery.of(context).orientation == Orientation.landscape) ? 3 : 2;\n\n    return Expanded(\n      child: Padding(\n        // 多列\n        padding: const EdgeInsets.fromLTRB(5, 3, 5, 1),\n        // 单列\n        // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),\n        child: Text(\n          bangumiItem.nameCn,\n          textAlign: TextAlign.start,\n          style: const TextStyle(\n            fontWeight: FontWeight.w500,\n            letterSpacing: 0.3,\n          ),\n          textScaler: ts.clamp(maxScaleFactor: 1.1),\n          maxLines: maxTextLines,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/bangumi_history_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:kazumi/modules/history/history_module.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/utils.dart';\n\n// 视频历史记录卡片 - 水平布局\nclass BangumiHistoryCardV extends StatefulWidget {\n  const BangumiHistoryCardV({\n    super.key,\n    required this.historyItem,\n    this.showDelete = true,\n    this.cardHeight = 120,\n    this.cardWidth,\n  });\n\n  final History historyItem;\n  final bool showDelete;\n  final double cardHeight;\n  final double? cardWidth;\n\n  @override\n  State<BangumiHistoryCardV> createState() => _BangumiHistoryCardVState();\n}\n\nclass _BangumiHistoryCardVState extends State<BangumiHistoryCardV> {\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  final HistoryController historyController = Modular.get<HistoryController>();\n\n  Widget propertyChip({\n    required String title,\n    required String value,\n    bool showTitle = false,\n  }) {\n    final message = '$title: $value';\n    return Chip(\n      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(32)),\n      backgroundColor: Theme.of(context).colorScheme.secondaryContainer,\n      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n      padding: const EdgeInsets.symmetric(horizontal: 2),\n      side: BorderSide.none,\n      label: Text(\n        showTitle ? message : value,\n        style: Theme.of(context).textTheme.labelSmall,\n        overflow: TextOverflow.ellipsis,\n        maxLines: 1,\n      ),\n    );\n  }\n\n  Widget buildImage(\n      BuildContext context, String imageUrl, double width, double height) {\n    final borderRadius = BorderRadius.circular(16);\n    Widget img = NetworkImgLayer(\n      src: imageUrl,\n      width: width,\n      height: height,\n    );\n    img = ClipRRect(\n      borderRadius: borderRadius,\n      child: img,\n    );\n    return img;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final double borderRadius = 18;\n    final double imageWidth = widget.cardHeight * 0.7;\n\n    return Card(\n      elevation: 1,\n      margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(borderRadius),\n      ),\n      clipBehavior: Clip.antiAlias,\n      color: theme.colorScheme.surface,\n      child: InkWell(\n        onTap: () async {\n          if (widget.showDelete) {\n            KazumiDialog.showToast(\n              message: '编辑模式',\n            );\n            return;\n          }\n          KazumiDialog.showLoading(\n            msg: '获取中',\n            barrierDismissible: Utils.isDesktop(),\n            onDismiss: () {\n              videoPageController.cancelQueryRoads();\n            },\n          );\n          bool flag = false;\n          for (Plugin plugin in pluginsController.pluginList) {\n            if (plugin.name == widget.historyItem.adapterName) {\n              videoPageController.currentPlugin = plugin;\n              flag = true;\n              break;\n            }\n          }\n          if (!flag) {\n            KazumiDialog.dismiss();\n            KazumiDialog.showToast(message: '未找到关联番剧源');\n            return;\n          }\n          videoPageController.bangumiItem = widget.historyItem.bangumiItem;\n          videoPageController.title =\n              widget.historyItem.bangumiItem.nameCn == ''\n                  ? widget.historyItem.bangumiItem.name\n                  : widget.historyItem.bangumiItem.nameCn;\n          videoPageController.src = widget.historyItem.lastSrc;\n          try {\n            await videoPageController.queryRoads(widget.historyItem.lastSrc,\n                videoPageController.currentPlugin.name);\n            KazumiDialog.dismiss();\n            Modular.to.pushNamed('/video/');\n          } catch (_) {\n            KazumiLogger().w(\"QueryManager: failed to query roads\");\n            KazumiDialog.dismiss();\n          }\n        },\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            buildImage(\n                context,\n                widget.historyItem.bangumiItem.images['large'] ?? '',\n                imageWidth,\n                widget.cardHeight),\n            const SizedBox(width: 12),\n            Expanded(\n              child: Padding(\n                padding:\n                    const EdgeInsets.symmetric(vertical: 2, horizontal: 6),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    const SizedBox(height: 6),\n                    Text(\n                      widget.historyItem.bangumiItem.nameCn == ''\n                          ? widget.historyItem.bangumiItem.name\n                          : widget.historyItem.bangumiItem.nameCn,\n                      style: Theme.of(context).textTheme.titleSmall?.copyWith(\n                            color: Theme.of(context).colorScheme.onSurface,\n                            fontWeight: FontWeight.bold,\n                          ),\n                      overflow: TextOverflow.ellipsis,\n                      maxLines: 1,\n                    ),\n                    const SizedBox(height: 12),\n                    Wrap(\n                      spacing: 4,\n                      runSpacing: 4,\n                      children: [\n                        propertyChip(\n                          title: '来源',\n                          value: widget.historyItem.adapterName,\n                          showTitle: true,\n                        ),\n                        propertyChip(\n                          title: '看到',\n                          value: widget.historyItem.lastWatchEpisodeName.isEmpty\n                              ? '第${widget.historyItem.lastWatchEpisode}话'\n                              : widget.historyItem.lastWatchEpisodeName,\n                          showTitle: true,\n                        ),\n                      ],\n                    )\n                  ],\n                ),\n              ),\n            ),\n            Column(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                if (!widget.showDelete) ...[\n                  CollectButton(\n                    onClose: () {\n                      FocusScope.of(context).unfocus();\n                    },\n                    bangumiItem: widget.historyItem.bangumiItem,\n                    color: Theme.of(context).colorScheme.onSecondaryContainer,\n                  ),\n                  IconButton(\n                    icon: Icon(\n                      Icons.open_in_new,\n                      color: Theme.of(context).colorScheme.onSecondaryContainer,\n                    ),\n                    tooltip: '番剧详情',\n                    onPressed: () {\n                      Modular.to.pushNamed(\n                        '/info/',\n                        arguments: widget.historyItem.bangumiItem,\n                      );\n                    },\n                  ),\n                ],\n                if (widget.showDelete)\n                  IconButton(\n                    icon: Icon(\n                      Icons.delete,\n                      color: Theme.of(context).colorScheme.onSecondaryContainer,\n                    ),\n                    onPressed: () {\n                      historyController.deleteHistory(widget.historyItem);\n                    },\n                  ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/bangumi_info_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_rating_bar/flutter_rating_bar.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\nimport 'package:fl_chart/fl_chart.dart';\nimport 'package:skeletonizer/skeletonizer.dart';\n\n// 视频卡片 - 水平布局\nclass BangumiInfoCardV extends StatefulWidget {\n  const BangumiInfoCardV({\n    super.key,\n    required this.bangumiItem,\n    required this.isLoading,\n    required this.showRating,\n  });\n\n  final BangumiItem bangumiItem;\n  final bool isLoading;\n  final bool showRating;\n\n  @override\n  State<BangumiInfoCardV> createState() => _BangumiInfoCardVState();\n}\n\nclass _BangumiInfoCardVState extends State<BangumiInfoCardV> {\n  int touchedIndex = -1;\n\n  Widget get voteBarChart {\n    return Flexible(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(\n            '  评分透视:',\n          ),\n          SizedBox(height: 16),\n          AspectRatio(\n            aspectRatio: 2,\n            child: BarChart(\n              duration: Duration(milliseconds: 80),\n              BarChartData(\n                // alignment: BarChartAlignment.spaceEvenly,\n                borderData: FlBorderData(show: false),\n                gridData: FlGridData(show: false),\n                barTouchData: BarTouchData(\n                  touchCallback: (FlTouchEvent event, barTouchResponse) {\n                    setState(() {\n                      if (!event.isInterestedForInteractions ||\n                          barTouchResponse == null ||\n                          barTouchResponse.spot == null) {\n                        touchedIndex = -1;\n                        return;\n                      }\n                      touchedIndex =\n                          barTouchResponse.spot!.touchedBarGroupIndex;\n                    });\n                  },\n                  touchTooltipData: BarTouchTooltipData(\n                    getTooltipColor: (_) =>\n                        Theme.of(context).colorScheme.inverseSurface,\n                    getTooltipItem: (group, groupIndex, rod, rodIndex) {\n                      var percentage =\n                          widget.bangumiItem.votesCount[groupIndex] /\n                              widget.bangumiItem.votes *\n                              100;\n                      return BarTooltipItem(\n                        '${percentage.toStringAsFixed(2)}% (${widget.bangumiItem.votesCount[groupIndex]}人)',\n                        TextStyle(\n                            color:\n                                Theme.of(context).colorScheme.onInverseSurface),\n                      );\n                    },\n                  ),\n                ),\n                barGroups: List<BarChartGroupData>.generate(\n                  10,\n                  (i) => BarChartGroupData(\n                    x: i + 1,\n                    barRods: [\n                      BarChartRodData(\n                        toY: widget.bangumiItem.votesCount[i].toDouble(),\n                        color: touchedIndex == i\n                            ? Theme.of(context).colorScheme.primary\n                            : Theme.of(context).disabledColor,\n                        width: 20,\n                        borderRadius:\n                            BorderRadius.vertical(top: Radius.circular(5)),\n                      )\n                    ],\n                    // showingTooltipIndicators: [0],\n                  ),\n                ),\n                titlesData: FlTitlesData(\n                  bottomTitles: AxisTitles(\n                    sideTitles: SideTitles(\n                      showTitles: true,\n                      reservedSize: 30,\n                      getTitlesWidget: (value, meta) => SideTitleWidget(\n                        meta: meta,\n                        space: 10,\n                        child: Text(value.toInt().toString()),\n                      ),\n                    ),\n                  ),\n                  topTitles: const AxisTitles(),\n                  leftTitles: const AxisTitles(),\n                  rightTitles: const AxisTitles(),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 300,\n      constraints: BoxConstraints(maxWidth: 950),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(\n            widget.bangumiItem.nameCn == ''\n                ? widget.bangumiItem.name\n                : (widget.bangumiItem.nameCn),\n            maxLines: 2,\n            overflow: TextOverflow.ellipsis,\n            style: Theme.of(context).textTheme.headlineSmall,\n          ),\n          SizedBox(height: 16),\n          Expanded(\n            child: Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Flexible(\n                  child: AspectRatio(\n                    aspectRatio: 0.65,\n                    child: LayoutBuilder(builder: (context, boxConstraints) {\n                      final double maxWidth = boxConstraints.maxWidth;\n                      final double maxHeight = boxConstraints.maxHeight;\n                      return Hero(\n                        transitionOnUserGestures: true,\n                        tag: widget.bangumiItem.id,\n                        child: NetworkImgLayer(\n                          src: widget.bangumiItem.images['large'] ?? '',\n                          width: maxWidth,\n                          height: maxHeight,\n                          fadeInDuration: const Duration(milliseconds: 0),\n                          fadeOutDuration: const Duration(milliseconds: 0),\n                        ),\n                      );\n                    }),\n                  ),\n                ),\n                SizedBox(width: 16),\n                Flexible(\n                  child: Skeletonizer(\n                    enabled: widget.isLoading,\n                    child: Column(\n                      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Column(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          children: [\n                            Text(\n                              '放送开始:',\n                            ),\n                            Text(\n                              widget.bangumiItem.airDate == ''\n                                  ? '2000-11-11' // Skeleton Loader 占位符\n                                  : widget.bangumiItem.airDate,\n                              style: TextStyle(\n                                fontSize: 20,\n                                fontWeight: FontWeight.bold,\n                                color: Theme.of(context).colorScheme.primary,\n                              ),\n                            ),\n                            SizedBox(height: 8),\n                            Text(\n                              widget.showRating\n                                  ? '${widget.bangumiItem.votes} 人评分:'\n                                  : '*** 人评分:',\n                            ),\n                            if (widget.isLoading)\n                              // Skeleton Loader 占位符\n                              Text(\n                                '10.0 ********',\n                                style: TextStyle(\n                                  fontSize: 20,\n                                  fontWeight: FontWeight.bold,\n                                  color: Theme.of(context).colorScheme.primary,\n                                ),\n                              ),\n                            if (!widget.isLoading)\n                              Row(\n                                children: [\n                                  Text(\n                                    widget.showRating\n                                        ? '${widget.bangumiItem.ratingScore}'\n                                        : '***',\n                                    style: TextStyle(\n                                      fontSize: 20,\n                                      fontWeight: FontWeight.bold,\n                                      color:\n                                          Theme.of(context).colorScheme.primary,\n                                    ),\n                                  ),\n                                  const SizedBox(width: 8),\n                                  RatingBarIndicator(\n                                    itemCount: 5,\n                                    rating: widget.showRating\n                                        ? widget.bangumiItem.ratingScore\n                                                .toDouble() /\n                                            2\n                                        : 0,\n                                    itemBuilder: (context, index) => Icon(\n                                      Icons.star_rounded,\n                                      color:\n                                          Theme.of(context).colorScheme.primary,\n                                    ),\n                                    itemSize: 20.0,\n                                  ),\n                                ],\n                              ),\n                            SizedBox(height: 8),\n                            Text(\n                              'Bangumi Ranked:',\n                            ),\n                            Text(\n                              widget.showRating\n                                  ? '#${widget.bangumiItem.rank}'\n                                  : '***',\n                              style: TextStyle(\n                                fontSize: 20,\n                                fontWeight: FontWeight.bold,\n                                color: Theme.of(context).colorScheme.primary,\n                              ),\n                            ),\n                          ],\n                        ),\n                        SizedBox(\n                          width: 120,\n                          height: 40,\n                          child: CollectButton.extend(\n                            bangumiItem: widget.bangumiItem,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n                if (widget.showRating &&\n                    MediaQuery.sizeOf(context).width >=\n                        LayoutBreakpoint.compact['width']! &&\n                    !widget.isLoading)\n                  voteBarChart,\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/bangumi_timeline_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\n\n/// 时间线番剧卡片\nclass BangumiTimelineCard extends StatelessWidget {\n  const BangumiTimelineCard({\n    super.key,\n    required this.bangumiItem,\n    required this.showRating,\n    this.onTap,\n    this.cardHeight = 120,\n    this.cardWidth,\n    this.enableHero = true,\n  });\n\n  final BangumiItem bangumiItem;\n  final bool showRating;\n  final VoidCallback? onTap;\n  final bool enableHero;\n  final double cardHeight;\n  final double? cardWidth;\n\n  @override\n  Widget build(BuildContext context) {\n    final isDesktop = Utils.isDesktop();\n    final isTablet = Utils.isTablet();\n    final theme = Theme.of(context);\n    final textScaler = MediaQuery.textScalerOf(context);\n    final double imageWidth = cardHeight * 0.7;\n    final double borderRadius = 18;\n\n    return Card(\n      elevation: 1,\n      margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(borderRadius),\n      ),\n      clipBehavior: Clip.antiAlias,\n      color: theme.colorScheme.surface,\n      child: InkWell(\n        borderRadius: BorderRadius.circular(borderRadius),\n        onTap: onTap ??\n            () {\n              Modular.to.pushNamed('/info/', arguments: bangumiItem);\n            },\n        child: SizedBox(\n          height: cardHeight,\n          width: cardWidth,\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              buildImage(context, bangumiItem.images['large'] ?? '', imageWidth, cardHeight),\n              Expanded(\n                child: Padding(\n                  padding:\n                      const EdgeInsets.symmetric(vertical: 10, horizontal: 12),\n                  child: buildInfo(context, textScaler, isDesktop, isTablet),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildImage(BuildContext context, String imageUrl, double width, double height) {\n    final borderRadius = BorderRadius.circular(16);\n    Widget img = NetworkImgLayer(\n      src: imageUrl,\n      width: width,\n      height: height,\n    );\n    if (enableHero) {\n      img = Hero(\n        tag: bangumiItem.id,\n        transitionOnUserGestures: true,\n        child: ClipRRect(\n          borderRadius: borderRadius,\n          child: img,\n        ),\n      );\n    } else {\n      img = ClipRRect(\n        borderRadius: borderRadius,\n        child: img,\n      );\n    }\n    return img;\n  }\n\n  Widget buildInfo(BuildContext context, TextScaler textScaler, bool isDesktop,\n      bool isTablet) {\n    final theme = Theme.of(context);\n    final colorScheme = theme.colorScheme;\n    final nameStyle =\n        theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600);\n    final subStyle = theme.textTheme.bodySmall\n        ?.copyWith(color: colorScheme.onSurfaceVariant);\n    final infoStyle =\n        theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500);\n    final maxLines = isDesktop ? 2 : 1;\n    final double spacing = isDesktop ? 8 : 4;\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        // 标题\n        Text(\n          bangumiItem.nameCn.isNotEmpty ? bangumiItem.nameCn : bangumiItem.name,\n          style: nameStyle,\n          maxLines: maxLines,\n          overflow: TextOverflow.ellipsis,\n          textScaler: textScaler.clamp(maxScaleFactor: 1.1),\n        ),\n        SizedBox(height: spacing),\n        // 简介\n        if (bangumiItem.summary.isNotEmpty || bangumiItem.info.isNotEmpty)\n          Padding(\n            padding: const EdgeInsets.only(bottom: 2),\n            child: DecoratedBox(\n              decoration: BoxDecoration(\n                color: colorScheme.primaryContainer.withAlpha((255 * 0.10).round()),\n                borderRadius: BorderRadius.circular(6),\n              ),\n              child: Padding(\n                padding: const EdgeInsets.only(bottom: 2),\n                child: Text(\n                  bangumiItem.info.isNotEmpty\n                      ? bangumiItem.info\n                      : bangumiItem.summary,\n                  style: theme.textTheme.labelMedium?.copyWith(\n                    color: colorScheme.primary,\n                    fontWeight: FontWeight.w500,\n                  ),\n                  maxLines: 3,\n                  overflow: TextOverflow.ellipsis,\n                  textScaler: textScaler.clamp(maxScaleFactor: 1.0),\n                ),\n              ),\n            ),\n          ),\n        const Spacer(),\n        Row(\n          children: [\n            if (showRating ? bangumiItem.ratingScore > 0 : true)\n              Row(\n                children: [\n                  Icon(Icons.star_rounded,\n                      size: 15, color: colorScheme.primary),\n                  const SizedBox(width: 2),\n                  Text(\n                      showRating\n                          ? bangumiItem.ratingScore.toStringAsFixed(1)\n                          : '***',\n                      style: infoStyle),\n                ],\n              ),\n            if (showRating ? bangumiItem.rank > 0 : true)\n              Padding(\n                padding: const EdgeInsets.only(left: 8),\n                child: Row(\n                  children: [\n                    Icon(Icons.leaderboard,\n                        size: 15, color: colorScheme.tertiary),\n                    const SizedBox(width: 2),\n                    Text(\n                        showRating ? 'Rank ${bangumiItem.rank}' : 'Rank ***',\n                        style: infoStyle),\n                  ],\n                ),\n              ),\n            const Spacer(),\n            if (showRating ? bangumiItem.votes > 0 : true)\n              Text(\n                  showRating ? '评分人数: ${bangumiItem.votes}' : '评分人数: ***',\n                  style: subStyle),\n          ],\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/character_card.dart",
    "content": "import 'package:kazumi/utils/utils.dart';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/modules/characters/character_item.dart';\nimport 'package:kazumi/pages/info/character_page.dart';\n\nclass CharacterCard extends StatelessWidget {\n  const CharacterCard({\n    super.key,\n    required this.characterItem,\n  });\n\n  final CharacterItem characterItem;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: CircleAvatar(\n        backgroundImage: characterItem.avator.grid.isEmpty\n            ? NetworkImage('https://bangumi.tv/img/info_only.png')\n            : NetworkImage(characterItem.avator.grid),\n      ),\n      title: Text(\n        characterItem.name,\n        overflow: TextOverflow.ellipsis,\n        maxLines: 1,\n      ),\n      subtitle: characterItem.actorList.isNotEmpty\n          ? Text(characterItem.actorList[0].name)\n          : null,\n      trailing: Text(characterItem.relation),\n      onTap: () {\n        showModalBottomSheet(\n            isScrollControlled: true,\n            constraints: BoxConstraints(\n                maxHeight: MediaQuery.of(context).size.height * 3 / 4,\n                maxWidth: (Utils.isDesktop() || Utils.isTablet())\n                    ? MediaQuery.of(context).size.width * 9 / 16\n                    : MediaQuery.of(context).size.width),\n            clipBehavior: Clip.antiAlias,\n            context: context,\n            builder: (context) {\n              return CharacterPage(characterID: characterItem.id);\n            });\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/character_comments_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bbcode/bbcode_widget.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/utils/utils.dart';\n\nclass CharacterCommentsCard extends StatelessWidget {\n  const CharacterCommentsCard({\n    super.key,\n    required this.commentItem,\n  });\n\n  final CharacterCommentItem commentItem;\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      // color: Theme.of(context).colorScheme.secondaryContainer,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                CircleAvatar(\n                  backgroundImage:\n                      NetworkImage(commentItem.comment.user.avatar.large),\n                ),\n                const SizedBox(width: 8),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(commentItem.comment.user.nickname),\n                    Text(Utils.dateFormat(commentItem.comment.createdAt)),\n                  ],\n                ),\n              ],\n            ),\n            const SizedBox(height: 8),\n            BBCodeWidget(bbcode: commentItem.comment.comment),\n            if (commentItem.replies.isNotEmpty)\n              ListView.builder(\n                // Don't know why but some device has bottom padding,\n                // needs to set to 0 manually.\n                padding: const EdgeInsets.only(bottom: 0),\n                physics: const NeverScrollableScrollPhysics(),\n                shrinkWrap: true,\n                itemCount: commentItem.replies.length,\n                itemBuilder: (context, index) {\n                  return Padding(\n                    padding: const EdgeInsets.only(left: 48),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: <Widget>[\n                        Divider(\n                          color: Theme.of(context).dividerColor.withAlpha(60),\n                        ),\n                        Row(\n                          children: [\n                            CircleAvatar(\n                              backgroundImage: NetworkImage(\n                                  commentItem.replies[index].user.avatar.large),\n                            ),\n                            const SizedBox(width: 8),\n                            Column(\n                              crossAxisAlignment: CrossAxisAlignment.start,\n                              children: [\n                                Text(commentItem.replies[index].user.nickname),\n                                Text(\n                                  Utils.dateFormat(\n                                      commentItem.replies[index].createdAt),\n                                ),\n                              ],\n                            ),\n                          ],\n                        ),\n                        const SizedBox(height: 8),\n                        BBCodeWidget(\n                            bbcode: commentItem.replies[index].comment),\n                      ],\n                    ),\n                  );\n                },\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/comments_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:flutter_rating_bar/flutter_rating_bar.dart';\nimport 'package:skeletonizer/skeletonizer.dart';\n\nclass CommentsCard extends StatelessWidget {\n  CommentsCard({\n    super.key,\n    required this.commentItem,\n  }) {\n    isBone = false;\n  }\n\n  CommentsCard.bone({\n    super.key,\n  }) {\n    isBone = true;\n    commentItem = null;\n  }\n\n  late final CommentItem? commentItem;\n  late final bool isBone;\n\n  @override\n  Widget build(BuildContext context) {\n    if (isBone) {\n      return Skeletonizer.zone(\n        enabled: true,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                const Bone.circle(size: 36),\n                const SizedBox(width: 8),\n                const Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Bone.text(width: 80),\n                    SizedBox(height: 8),\n                    Bone.text(width: 60),\n                  ],\n                ),\n              ],\n            ),\n            SizedBox(height: 8),\n            const Bone.multiText(lines: 2),\n            Divider(thickness: 0.5, indent: 10, endIndent: 10),\n          ],\n        ),\n      );\n    }\n    return SelectionArea(\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                CircleAvatar(\n                  backgroundImage: NetworkImage(commentItem!.user.avatar.large),\n                ),\n                const SizedBox(width: 8),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(commentItem!.user.nickname),\n                    Text(Utils.dateFormat(commentItem!.comment.updatedAt)),\n                  ],\n                ),\n                Expanded(child: Container(height: 10)),\n                RatingBarIndicator(\n                  itemCount: 5,\n                  rating: commentItem!.comment.rate.toDouble() / 2,\n                  itemBuilder: (context, index) => const Icon(\n                    Icons.star_rounded,\n                  ),\n                  itemSize: 20.0,\n                ),\n              ],\n            ),\n            const SizedBox(height: 8),\n            Text(commentItem!.comment.comment),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/episode_comments_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bbcode/bbcode_widget.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/utils/utils.dart';\n\nclass EpisodeCommentsCard extends StatelessWidget {\n  const EpisodeCommentsCard({\n    super.key,\n    required this.commentItem,\n  });\n\n  final EpisodeCommentItem commentItem;\n\n  @override\n  Widget build(BuildContext context) {\n    // 对 用户评论 做判空操作，如果为空则显示“用户已删除”\n    String userComment = commentItem.comment.comment;\n    if (userComment.isEmpty) {\n      userComment = \"<用户已删除>\";\n    }\n\n    return Card(\n      // color: Theme.of(context).colorScheme.secondaryContainer,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                CircleAvatar(\n                  backgroundImage:\n                      NetworkImage(commentItem.comment.user.avatar.large),\n                ),\n                const SizedBox(width: 8),\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(commentItem.comment.user.nickname),\n                    Text(Utils.dateFormat(commentItem.comment.createdAt)),\n                  ],\n                ),\n              ],\n            ),\n            const SizedBox(height: 8),\n            BBCodeWidget(bbcode: userComment),\n            if (commentItem.replies.isNotEmpty)\n              ListView.builder(\n                // Don't know why but ohos has bottom padding,\n                // needs to set to 0 manually.\n                padding: const EdgeInsets.only(bottom: 0),\n                physics: const NeverScrollableScrollPhysics(),\n                shrinkWrap: true,\n                itemCount: commentItem.replies.length,\n                itemBuilder: (context, index) {\n                  return Padding(\n                    padding: const EdgeInsets.only(left: 48),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: <Widget>[\n                        Divider(\n                          color: Theme.of(context).dividerColor.withAlpha(60),\n                        ),\n                        Row(\n                          children: [\n                            CircleAvatar(\n                              backgroundImage: NetworkImage(\n                                  commentItem.replies[index].user.avatar.large),\n                            ),\n                            const SizedBox(width: 8),\n                            Column(\n                              crossAxisAlignment: CrossAxisAlignment.start,\n                              children: [\n                                Text(commentItem.replies[index].user.nickname),\n                                Text(\n                                  Utils.dateFormat(\n                                      commentItem.replies[index].createdAt),\n                                ),\n                              ],\n                            ),\n                          ],\n                        ),\n                        const SizedBox(height: 8),\n                        BBCodeWidget(\n                            bbcode: commentItem.replies[index].comment),\n                      ],\n                    ),\n                  );\n                },\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/network_img_layer.dart",
    "content": "import 'package:cached_network_image/cached_network_image.dart';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/extension.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass NetworkImgLayer extends StatelessWidget {\n  const NetworkImgLayer({\n    super.key,\n    this.src,\n    required this.width,\n    required this.height,\n    this.type,\n    this.fadeOutDuration,\n    this.fadeInDuration,\n    this.quality,\n    this.origAspectRatio,\n  });\n\n  final String? src;\n  final double width;\n  final double height;\n  final String? type;\n  final Duration? fadeOutDuration;\n  final Duration? fadeInDuration;\n  final int? quality;\n  final double? origAspectRatio;\n\n  @override\n  Widget build(BuildContext context) {\n    final String imageUrl = src ?? '';\n\n    //// We need this to shink memory usage\n    int? memCacheWidth, memCacheHeight;\n    double aspectRatio = (width / height).toDouble();\n\n    void setMemCacheSizes() {\n      if (aspectRatio > 1) {\n        memCacheHeight = height.cacheSize(context);\n      } else if (aspectRatio < 1) {\n        memCacheWidth = width.cacheSize(context);\n      } else {\n        if (origAspectRatio != null && origAspectRatio! > 1) {\n          memCacheWidth = width.cacheSize(context);\n        } else if (origAspectRatio != null && origAspectRatio! < 1) {\n          memCacheHeight = height.cacheSize(context);\n        } else {\n          memCacheWidth = width.cacheSize(context);\n          memCacheHeight = height.cacheSize(context);\n        }\n      }\n    }\n\n    setMemCacheSizes();\n\n    if (memCacheWidth == null && memCacheHeight == null) {\n      memCacheWidth = width.toInt();\n    }\n\n    return src != '' && src != null\n        ? ClipRRect(\n            clipBehavior: Clip.antiAlias,\n            borderRadius: BorderRadius.circular(\n              type == 'avatar'\n                  ? 50\n                  : type == 'emote'\n                      ? 0\n                      : StyleString.imgRadius.x,\n            ),\n            child: CachedNetworkImage(\n              imageUrl: imageUrl,\n              width: width,\n              height: height,\n              memCacheWidth: memCacheWidth,\n              memCacheHeight: memCacheHeight,\n              fit: BoxFit.cover,\n              fadeOutDuration:\n                  fadeOutDuration ?? const Duration(milliseconds: 120),\n              fadeInDuration:\n                  fadeInDuration ?? const Duration(milliseconds: 120),\n              filterQuality: FilterQuality.high,\n              errorListener: (e) {\n                KazumiLogger().w(\"NetworkImage: network image load error\", error: e);\n              },\n              errorWidget: (BuildContext context, String url, Object error) =>\n                  placeholder(context),\n              placeholder: (BuildContext context, String url) =>\n                  placeholder(context),\n            ))\n        : placeholder(context);\n  }\n\n  Widget placeholder(BuildContext context) {\n    return Container(\n      width: width,\n      height: height,\n      clipBehavior: Clip.antiAlias,\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.onInverseSurface.withValues(alpha: 0.4),\n        borderRadius: BorderRadius.circular(type == 'avatar'\n            ? 50\n            : type == 'emote'\n                ? 0\n                : StyleString.imgRadius.x),\n      ),\n      child: type == 'bg'\n          ? const SizedBox()\n          : Center(\n              child: Image.asset(\n                type == 'avatar'\n                    ? 'assets/images/noface.jpeg'\n                    : 'assets/images/loading.png',\n                width: width,\n                height: height,\n                cacheWidth: width.cacheSize(context),\n                cacheHeight: height.cacheSize(context),\n              ),\n            ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/palette_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:material_color_utilities/material_color_utilities.dart';\n\nclass PaletteCard extends StatefulWidget {\n  final Color color;\n  final bool selected;\n\n  const PaletteCard({\n    super.key,\n    required this.color,\n    required this.selected,\n  });\n\n  @override\n  State<StatefulWidget> createState() => _PaletteCardState();\n}\n\nclass _PaletteCardState extends State<PaletteCard> {\n  @override\n  Widget build(BuildContext context) {\n    final Hct hct = Hct.fromInt(widget.color.value);\n    final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());\n    final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());\n    final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());\n    final checkbox = Color(Hct.from(hct.hue, 30.0, 40.0).toInt());\n    return SizedBox(\n      width: 70,\n      height: 70,\n      child: Stack(\n        children: [\n          Card(\n            elevation: 0,\n            child: Container(\n              padding: const EdgeInsets.all(10),\n              child: ClipOval(\n                child: Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  children: [\n                    Expanded(\n                      child: Container(\n                        color: primary,\n                      ),\n                    ),\n                    Expanded(\n                      child: Row(\n                        mainAxisAlignment: MainAxisAlignment.center,\n                        children: [\n                          Expanded(\n                            child: Container(\n                              color: tertiary,\n                            ),\n                          ),\n                          Expanded(\n                            child: Container(\n                              color: primaryContainer,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          if (widget.selected)\n            Center(\n              child: Container(\n                width: 25,\n                height: 25,\n                decoration: BoxDecoration(\n                  color: checkbox,\n                  shape: BoxShape.circle,\n                ),\n                child: Icon(\n                  Icons.check_rounded,\n                  color: Theme.of(context).colorScheme.surfaceContainerLow,\n                  size: 12,\n                ),\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/card/staff_card.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/modules/staff/staff_item.dart';\n\nclass StaffCard extends StatelessWidget {\n  const StaffCard({\n    super.key,\n    required this.staffFullItem,\n  });\n\n  final StaffFullItem staffFullItem;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: CircleAvatar(\n        backgroundImage: staffFullItem.staff.images?.grid == null\n            ? NetworkImage('https://bangumi.tv/img/info_only.png')\n            : NetworkImage(staffFullItem.staff.images!.grid),\n      ),\n      title: Text(\n        staffFullItem.staff.name,\n        overflow: TextOverflow.ellipsis,\n        maxLines: 1,\n      ),\n      subtitle: staffFullItem.staff.nameCN.isNotEmpty\n          ? Text(staffFullItem.staff.nameCN)\n          : null,\n      trailing: Text(staffFullItem.positions.isNotEmpty\n          ? (staffFullItem.positions[0].type.cn)\n          : ''),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/dialog/dialog_helper.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/utils/constants.dart';\n\n// A simple dialog helper class to show dialogs and toasts based on flutter native implementation (replace flutter_smart_dialog)\n// flutter_smart_dialog use overlays and self-managed route stack to show dialogs.\n// It's powerful but can't behave like the default showDialog, e.g. the lack of mask animation. the lack of snackbar.\n// Use the implementation should be careful, because shared route stack with the whole app, it may cause some unexpected behaviors.\n// Don't use it in double PopScope widget.\nclass KazumiDialog {\n  /// The global observer that tracks contexts across the application\n  static final KazumiDialogObserver observer = KazumiDialogObserver();\n\n  KazumiDialog._internal();\n\n  static Future<T?> show<T>({\n    BuildContext? context,\n    bool? clickMaskDismiss,\n    VoidCallback? onDismiss,\n    required WidgetBuilder builder,\n  }) async {\n    final ctx = context ?? observer.currentContext;\n    if (ctx != null && ctx.mounted) {\n      try {\n        final result = await showDialog<T>(\n          context: ctx,\n          barrierDismissible: clickMaskDismiss ?? true,\n          builder: builder,\n          routeSettings: const RouteSettings(name: 'KazumiDialog'),\n        );\n        onDismiss?.call();\n        return result;\n      } catch (e) {\n        debugPrint('Kazumi Dialog Error: Failed to show dialog: $e');\n        return null;\n      }\n    } else {\n      debugPrint(\n          'Kazumi Dialog Error: No context available to show the dialog');\n      return null;\n    }\n  }\n\n  static void showToast({\n    required String message,\n    BuildContext? context,\n    bool showActionButton = false,\n    String? actionLabel,\n    Function()? onActionPressed,\n    Duration duration = const Duration(seconds: 2),\n  }) {\n    final ctx = context ?? observer.scaffoldContext;\n    if (ctx != null && ctx.mounted) {\n      try {\n        ScaffoldMessenger.of(ctx)\n          ..removeCurrentSnackBar()\n          ..showSnackBar(\n            SnackBar(\n              content: Text(message),\n              behavior: SnackBarBehavior.floating,\n              width: MediaQuery.sizeOf(ctx).width >\n                      LayoutBreakpoint.medium['width']!\n                  ? 600\n                  : null,\n              duration: duration,\n              action: showActionButton\n                  ? SnackBarAction(\n                      label: actionLabel ?? 'Dismiss',\n                      onPressed: () {\n                        onActionPressed?.call();\n                        ScaffoldMessenger.of(ctx).hideCurrentSnackBar();\n                      },\n                    )\n                  : null,\n            ),\n          );\n      } catch (e) {\n        debugPrint('Kazumi Dialog Error: Failed to show toast: $e');\n      }\n    } else {\n      debugPrint(\n          'Kazumi Dialog Error: No Scaffold context available to show Toast');\n    }\n  }\n\n  static Future<void> showLoading({\n    BuildContext? context,\n    String? msg,\n    bool barrierDismissible = false,\n    Function()? onDismiss,\n  }) async {\n    final ctx = context ?? observer.currentContext;\n    if (ctx != null && ctx.mounted) {\n      try {\n        await showDialog(\n          context: ctx,\n          barrierDismissible: barrierDismissible,\n          builder: (BuildContext context) {\n            return Center(\n              child: Card(\n                elevation: 8.0,\n                shape: RoundedRectangleBorder(\n                    borderRadius: BorderRadius.circular(12)),\n                child: Padding(\n                  padding: const EdgeInsets.all(24.0),\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      const CircularProgressIndicator(),\n                      const SizedBox(height: 16),\n                      Text(\n                        msg ?? 'Loading...',\n                        style: const TextStyle(fontSize: 16),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n          routeSettings: const RouteSettings(name: 'KazumiDialog'),\n        );\n        onDismiss?.call();\n      } catch (e) {\n        debugPrint('Kazumi Dialog Error: Failed to show loading dialog: $e');\n      }\n    } else {\n      debugPrint(\n          'Kazumi Dialog Error: No context available to show the loading dialog');\n    }\n  }\n\n  static Future<T?> showBottomSheet<T>({\n    BuildContext? context,\n    required WidgetBuilder builder,\n    Color? backgroundColor,\n    double? elevation,\n    ShapeBorder? shape,\n    Clip? clipBehavior,\n    BoxConstraints? constraints,\n    Color? barrierColor,\n    bool isScrollControlled = false,\n    bool useRootNavigator = true,\n    bool isDismissible = true,\n    bool enableDrag = true,\n    RouteSettings? routeSettings,\n    AnimationController? transitionAnimationController,\n    Offset? anchorPoint,\n    bool useSafeArea = false,\n  }) async {\n    // Use provided context first, then root context, then fallback to current context\n    final ctx = context ?? observer.rootContext ?? observer.currentContext;\n    if (ctx != null && ctx.mounted) {\n      try {\n        final result = await showModalBottomSheet<T>(\n          context: ctx,\n          builder: builder,\n          backgroundColor: backgroundColor,\n          elevation: elevation,\n          shape: shape,\n          clipBehavior: clipBehavior,\n          constraints: constraints,\n          barrierColor: barrierColor,\n          isScrollControlled: isScrollControlled,\n          useRootNavigator: useRootNavigator,\n          isDismissible: isDismissible,\n          enableDrag: enableDrag,\n          routeSettings:\n              routeSettings ?? const RouteSettings(name: 'KazumiBottomSheet'),\n          transitionAnimationController: transitionAnimationController,\n          anchorPoint: anchorPoint,\n          useSafeArea: useSafeArea,\n        );\n        return result;\n      } catch (e) {\n        debugPrint('Kazumi Dialog Error: Failed to show bottom sheet: $e');\n        return null;\n      }\n    } else {\n      debugPrint(\n          'Kazumi Dialog Error: No context available to show the bottom sheet');\n      return null;\n    }\n  }\n\n  // 在存在返回值时弹出并附带返回值\n  static void dismiss<T>({T? popWith}) {\n    if (observer.hasKazumiDialog && observer.kazumiDialogContext != null) {\n      try {\n        Navigator.of(observer.kazumiDialogContext!).pop(popWith);\n      } catch (e) {\n        debugPrint('Kazumi Dialog Error: Failed to dismiss dialog: $e');\n      }\n    } else {\n      debugPrint('Kazumi Dialog Debug: No active KazumiDialog to dismiss');\n    }\n  }\n\n  /// Shows a non-dismissible timed success dialog with a linear progress\n  /// countdown, then auto-dismisses when the countdown completes.\n  ///\n  /// The caller is responsible for dismissing any currently-open dialog\n  /// BEFORE calling this method.\n  ///\n  /// [onComplete] is invoked inside [onDismiss] after the countdown finishes\n  /// (or if the dialog is dismissed for any other reason), ensuring it runs\n  /// exactly once and resources are always cleaned up.\n  static void showTimedSuccessDialog({\n    required String title,\n    required String message,\n    required VoidCallback onComplete,\n    Duration duration = const Duration(seconds: 3),\n  }) {\n    final progressNotifier = ValueNotifier<double>(0.0);\n    Timer? countdownTimer;\n    final totalMs = duration.inMilliseconds;\n    final stopwatch = Stopwatch()..start();\n    countdownTimer = Timer.periodic(const Duration(milliseconds: 16), (t) {\n      final elapsed = stopwatch.elapsedMilliseconds;\n      progressNotifier.value = (elapsed / totalMs).clamp(0.0, 1.0);\n      if (elapsed >= totalMs) {\n        t.cancel();\n        KazumiDialog.dismiss();\n      }\n    });\n    KazumiDialog.show(\n      clickMaskDismiss: false,\n      onDismiss: () {\n        countdownTimer?.cancel();\n        progressNotifier.dispose();\n        onComplete();\n      },\n      builder: (context) => Dialog(\n        clipBehavior: Clip.antiAlias,\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28),\n          child: SizedBox(\n            width: 320,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                Icon(\n                  Icons.check_circle_rounded,\n                  size: 52,\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n                const SizedBox(height: 16),\n                Text(\n                  title,\n                  style: Theme.of(context).textTheme.titleLarge,\n                ),\n                const SizedBox(height: 6),\n                Text(\n                  message,\n                  style: Theme.of(context).textTheme.bodyMedium,\n                ),\n                const SizedBox(height: 24),\n                ValueListenableBuilder<double>(\n                  valueListenable: progressNotifier,\n                  builder: (context, value, _) => LinearProgressIndicator(\n                    value: value,\n                    borderRadius: BorderRadius.circular(4),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n/// Navigator observer to track contexts and dialog routes\nclass KazumiDialogObserver extends NavigatorObserver {\n  /// List of active dialog routes\n  final List<Route<dynamic>> _kazumiDialogRoutes = [];\n\n  /// The most recent context from any MaterialPageRoute or PopupRoute\n  BuildContext? _currentContext;\n\n  /// The most recent context from any route containing a Scaffold\n  BuildContext? _scaffoldContext;\n\n  /// The root context of the app (for bottom sheets to cover the entire app)\n  BuildContext? _rootContext;\n\n  BuildContext? get currentContext => _currentContext;\n\n  BuildContext? get scaffoldContext => _scaffoldContext ?? _currentContext;\n\n  /// Get the root context for bottom sheets, fallback to scaffold context, then current context\n  BuildContext? get rootContext =>\n      _rootContext ?? _scaffoldContext ?? _currentContext;\n\n  bool get hasKazumiDialog => _kazumiDialogRoutes.isNotEmpty;\n\n  BuildContext? get kazumiDialogContext => _kazumiDialogRoutes.isNotEmpty\n      ? _kazumiDialogRoutes.last.navigator?.context\n      : null;\n\n  @override\n  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {\n    super.didPush(route, previousRoute);\n\n    /// workaround for #533\n    /// we can't remove snackbar when push a new route\n    /// otherwise, framework will throw an exception, and can't be caught\n    /// need other way to remove snackbar here\n    // _removeCurrentSnackBar(previousRoute);\n    if (_isKazumiDialogRoute(route)) {\n      _kazumiDialogRoutes.add(route);\n    }\n    if (route.navigator?.context != null) {\n      _updateContexts(route.navigator!.context, route);\n    }\n  }\n\n  @override\n  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {\n    super.didPop(route, previousRoute);\n    _removeCurrentSnackBar(route);\n    if (_isKazumiDialogRoute(route)) {\n      _kazumiDialogRoutes.remove(route);\n    }\n    if (previousRoute?.navigator?.context != null) {\n      _updateContexts(previousRoute!.navigator!.context, previousRoute);\n    }\n  }\n\n  @override\n  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {\n    super.didReplace(newRoute: newRoute, oldRoute: oldRoute);\n    if (_isKazumiDialogRoute(oldRoute!)) {\n      _kazumiDialogRoutes.remove(oldRoute);\n    }\n    if (_isKazumiDialogRoute(newRoute!)) {\n      _kazumiDialogRoutes.add(newRoute);\n    }\n    if (newRoute.navigator?.context != null) {\n      _updateContexts(newRoute.navigator!.context, newRoute);\n    }\n  }\n\n  @override\n  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {\n    super.didRemove(route, previousRoute);\n\n    if (_isKazumiDialogRoute(route)) {\n      _kazumiDialogRoutes.remove(route);\n    }\n\n    if (previousRoute?.navigator?.context != null) {\n      _updateContexts(previousRoute!.navigator!.context, previousRoute);\n    }\n  }\n\n  void _updateContexts(BuildContext context, Route<dynamic> route) {\n    _currentContext = context;\n    if (_hasScaffold(context)) {\n      _scaffoldContext = context;\n      // Always update root context with scaffold contexts to ensure we have the most recent one\n      // This helps ensure bottom sheets appear at the app level\n      _rootContext = context;\n    }\n  }\n\n  bool _hasScaffold(BuildContext context) {\n    return Scaffold.maybeOf(context) != null;\n  }\n\n  bool _isKazumiDialogRoute(Route<dynamic> route) {\n    return route.settings.name == 'KazumiDialog' ||\n        route.settings.name == 'KazumiBottomSheet';\n  }\n\n  void _removeCurrentSnackBar(Route<dynamic>? route) {\n    if (route?.navigator?.context != null) {\n      try {\n        ScaffoldMessenger.maybeOf(route!.navigator!.context)\n            ?.removeCurrentSnackBar();\n      } catch (_) {}\n    }\n  }\n}\n"
  },
  {
    "path": "lib/bean/settings/color_type.dart",
    "content": "import 'package:flutter/material.dart';\n\nfinal List<Map<String, dynamic>> colorThemeTypes = [\n  {'color': Colors.green, 'label': '默认'},\n  {'color': Colors.teal, 'label': '青色'},\n  {'color': Colors.blue, 'label': '蓝色'},\n  {'color': Colors.indigo, 'label': '靛蓝色'},\n  {'color': const Color(0xff6750a4), 'label': '紫罗兰色'},\n  {'color': Colors.pink, 'label': '粉红色'},\n  {'color': Colors.yellow, 'label': '黄色'},\n  {'color': Colors.orange, 'label': '橙色'},\n  {'color': Colors.deepOrange, 'label': '深橙色'},\n];\n"
  },
  {
    "path": "lib/bean/settings/theme_provider.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/utils/constants.dart';\n\nclass ThemeProvider extends ChangeNotifier {\n  ThemeMode themeMode = ThemeMode.system;\n  bool useDynamicColor = false;\n  late ThemeData light;\n  late ThemeData dark;\n  String? currentFontFamily = customAppFontFamily;\n\n  void setTheme(ThemeData light, ThemeData dark, {bool notify = true}) {\n    this.light = light;\n    this.dark = dark;\n    if (notify) notifyListeners();\n  }\n\n  void setThemeMode(ThemeMode mode, {bool notify = true}) {\n    themeMode = mode;\n    if (notify) notifyListeners();\n  }\n\n  void setDynamic(bool useDynamicColor, {bool notify = true}) {\n    this.useDynamicColor = useDynamicColor;\n    if (notify) notifyListeners();\n  }\n\n  void setFontFamily(bool useSystemFont, {bool notify = true}) {\n    currentFontFamily = useSystemFont ? null : customAppFontFamily;\n    if (notify) notifyListeners();\n  }\n}\n"
  },
  {
    "path": "lib/bean/widget/collect_button.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass CollectButton extends StatefulWidget {\n  CollectButton({\n    super.key,\n    required this.bangumiItem,\n    this.color = Colors.white,\n    this.onOpen,\n    this.onClose,\n  }) {\n    isExtended = false;\n  }\n\n  CollectButton.extend({\n    super.key,\n    required this.bangumiItem,\n    this.color = Colors.white,\n    this.onOpen,\n    this.onClose,\n  }) {\n    isExtended = true;\n  }\n\n  final BangumiItem bangumiItem;\n  final Color color;\n  late final bool isExtended;\n  final void Function()? onOpen;\n  final void Function()? onClose;\n\n  @override\n  State<CollectButton> createState() => _CollectButtonState();\n}\n\nclass _CollectButtonState extends State<CollectButton> {\n  // 1. 在看\n  // 2. 想看\n  // 3. 搁置\n  // 4. 看过\n  // 5. 抛弃\n  late int collectType;\n  final CollectController collectController = Modular.get<CollectController>();\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  String getTypeStringByInt(int collectType) {\n    switch (collectType) {\n      case 1:\n        return \"在看\";\n      case 2:\n        return \"想看\";\n      case 3:\n        return \"搁置\";\n      case 4:\n        return \"看过\";\n      case 5:\n        return \"抛弃\";\n      default:\n        return \"未追\";\n    }\n  }\n\n  IconData getIconByInt(int collectType) {\n    switch (collectType) {\n      case 1:\n        return Icons.favorite;\n      case 2:\n        return Icons.star_rounded;\n      case 3:\n        return Icons.pending_actions;\n      case 4:\n        return Icons.done;\n      case 5:\n        return Icons.heart_broken;\n      default:\n        return Icons.favorite_border;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    collectType = collectController.getCollectType(widget.bangumiItem);\n    return MenuAnchor(\n      consumeOutsideTap: true,\n      onClose: widget.onClose,\n      onOpen: widget.onOpen,\n      crossAxisUnconstrained: false,\n      builder: (_, MenuController controller, __) {\n        if (widget.isExtended) {\n          return FilledButton.icon(\n            onPressed: () {\n              if (controller.isOpen) {\n                controller.close();\n              } else {\n                controller.open();\n              }\n            },\n            icon: Icon(getIconByInt(collectType)),\n            label: Text(getTypeStringByInt(collectType)),\n          );\n        } else {\n          return IconButton(\n            onPressed: () {\n              if (controller.isOpen) {\n                controller.close();\n              } else {\n                controller.open();\n              }\n            },\n            icon: Icon(\n              getIconByInt(collectType),\n              color: widget.color,\n            ),\n          );\n        }\n      },\n      menuChildren: List<MenuItemButton>.generate(\n        6,\n        (int index) => MenuItemButton(\n          onPressed: () {\n            if (index != collectType && mounted) {\n              collectController.addCollect(widget.bangumiItem, type: index);\n              setState(() {});\n            }\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Icon(\n                    getIconByInt(index),\n                    color: index == collectType\n                        ? Theme.of(context).colorScheme.primary\n                        : null,\n                  ),\n                  SizedBox(width: 4),\n                  Text(\n                    ' ${getTypeStringByInt(index)}',\n                    style: TextStyle(\n                      color: index == collectType\n                          ? Theme.of(context).colorScheme.primary\n                          : null,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/widget/custom_dropdown_menu.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// A custom dropdown menu widget that provides smooth animations without flickering.\n/// \n/// This widget was created to solve the visual flickering issue in Flutter's built-in\n/// [PopupMenuButton] where menu items are rendered before the animation completes,\n/// causing an uncoordinated visual effect.\nclass CustomDropdownMenu extends StatelessWidget {\n  final Offset offset;\n  final Size buttonSize;\n  final Animation<double> animation;\n  final List<String> items;\n  final String Function(String) itemBuilder;\n  final double? maxHeight;\n  /// Minimum width constraint for the menu. Defaults to 140.\n  /// Note: If [maxWidth] is less than [minWidth], [minWidth] will be used as both min and max.\n  final double? minWidth;\n  /// Maximum width constraint for the menu. Defaults to 200.\n  /// Note: If this value is less than [minWidth], it will be adjusted to equal [minWidth].\n  final double? maxWidth;\n  final double gap;\n\n  const CustomDropdownMenu({\n    super.key,\n    required this.offset,\n    required this.buttonSize,\n    required this.animation,\n    required this.items,\n    required this.itemBuilder,\n    this.maxHeight,\n    this.minWidth,\n    this.maxWidth,\n    this.gap = 4,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n\n    // Ensure width constraints are valid (minWidth <= maxWidth)\n    final computedMinWidth = minWidth ?? 140;\n    final computedMaxWidth = maxWidth ?? 200;\n    final normalizedMinWidth = computedMinWidth;\n    final normalizedMaxWidth = computedMaxWidth < computedMinWidth \n        ? computedMinWidth \n        : computedMaxWidth;\n\n    return GestureDetector(\n      onTap: () => Navigator.pop(context),\n      behavior: HitTestBehavior.opaque,\n      child: Stack(\n        children: [\n          Positioned(\n            left: offset.dx,\n            top: offset.dy + buttonSize.height + gap,\n            child: Material(\n              elevation: 6,\n              borderRadius: BorderRadius.circular(8),\n              color: theme.colorScheme.surface,\n              surfaceTintColor: Colors.transparent,\n              shadowColor: Colors.black26,\n              child: AnimatedBuilder(\n                animation: animation,\n                builder: (context, child) {\n                  final curvedValue =\n                      Curves.easeOutCubic.transform(animation.value);\n                  return ClipRect(\n                    child: Align(\n                      alignment: Alignment.topCenter,\n                      heightFactor: curvedValue,\n                      child: Opacity(\n                        opacity: curvedValue,\n                        child: child,\n                      ),\n                    ),\n                  );\n                },\n                child: Container(\n                  constraints: BoxConstraints(\n                    maxHeight: maxHeight ?? 350,\n                    minWidth: normalizedMinWidth,\n                    maxWidth: normalizedMaxWidth,\n                  ),\n                  child: ListView.builder(\n                    padding: const EdgeInsets.symmetric(vertical: 8),\n                    shrinkWrap: true,\n                    itemCount: items.length,\n                    itemBuilder: (context, index) {\n                      final itemValue = items[index];\n                      final displayText = itemBuilder(itemValue);\n                      return InkWell(\n                        onTap: () => Navigator.pop(context, itemValue),\n                        child: Padding(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 16,\n                            vertical: 12,\n                          ),\n                          child: Text(\n                            displayText,\n                            style: const TextStyle(fontSize: 14),\n                          ),\n                        ),\n                      );\n                    },\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/widget/embedded_native_control_area.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/utils/storage.dart';\n\nclass EmbeddedNativeControlArea extends StatefulWidget {\n  /// The widget won't draw anything, just a placeholder for native window control.\n  /// It only works on macOS at the moment.\n  /// windows and linux have no way to embed native window control into flutter view.\n  const EmbeddedNativeControlArea({\n    super.key,\n    required this.child,\n    this.requireOffset = true,\n  });\n\n  final Widget child;\n  final bool requireOffset;\n\n  @override\n  State<StatefulWidget> createState() => _EmbeddedNativeControlAreaState();\n}\n\nclass _EmbeddedNativeControlAreaState extends State<EmbeddedNativeControlArea> {\n  bool showWindowButton =\n      GStorage.setting.get(SettingBoxKey.showWindowButton, defaultValue: false);\n\n  EdgeInsets get getInsets {\n    if (!showWindowButton) {\n      return EdgeInsets.zero;\n    }\n    if (!widget.requireOffset) {\n      return EdgeInsets.zero;\n    }\n    if (Platform.isMacOS) {\n      return const EdgeInsets.only(top: 22);\n    } else {\n      return EdgeInsets.zero;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: getInsets,\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/widget/error_widget.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass GeneralErrorWidget extends StatelessWidget {\n  const GeneralErrorWidget({\n    required this.errMsg,\n    this.actions,\n    super.key,\n  });\n\n  final String errMsg;\n  final List<Widget>? actions;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.center,\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints constraints) {\n            return ConstrainedBox(\n              constraints: BoxConstraints(\n                maxWidth: constraints.maxWidth * 2 / 3,\n              ),\n              child: Text(\n                errMsg,\n                textAlign: TextAlign.center,\n                style: Theme.of(context).textTheme.titleSmall,\n              ),\n            );\n          },\n        ),\n        const SizedBox(height: 20),\n        if (actions != null)\n          Wrap(\n            alignment: WrapAlignment.center,\n            spacing: 8,\n            runSpacing: 8,\n            children: actions!,\n          ),\n      ],\n    );\n  }\n}\n\nclass GeneralErrorButton extends StatelessWidget {\n  const GeneralErrorButton({\n    super.key,\n    required this.onPressed,\n    required this.text,\n  });\n\n  final Function() onPressed;\n  final String text;\n\n  @override\n  Widget build(BuildContext context) {\n    return FilledButton.tonal(\n      onPressed: onPressed,\n      style: ButtonStyle(\n        backgroundColor: WidgetStateProperty.resolveWith((_) {\n          return Theme.of(context).colorScheme.primary.withAlpha(20);\n        }),\n      ),\n      child: Text(\n        text,\n        style: TextStyle(color: Theme.of(context).colorScheme.primary),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/bean/widget/scrollable_wrapper.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/gestures.dart';\n\n/// 滚动容器\n/// 支持鼠标滚轮滚动和拖动滚动\n/// 传入ListView的scrollController\nclass ScrollableWrapper extends StatelessWidget {\n  final Widget child;\n  final ScrollController scrollController;\n\n  const ScrollableWrapper({\n    super.key,\n    required this.child,\n    required this.scrollController,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      child: Listener(\n        onPointerSignal: (pointerSignal) {\n          // 鼠标滚轮滚动\n          if (pointerSignal is PointerScrollEvent &&\n              scrollController.hasClients) {\n            scrollController.position.moveTo(\n              scrollController.offset + pointerSignal.scrollDelta.dy,\n              curve: Curves.linear,\n            );\n          }\n        },\n        child: GestureDetector(\n          onPanUpdate: (details) {\n            // 拖动滚动\n            if (scrollController.hasClients) {\n              scrollController.position.moveTo(\n                scrollController.offset - details.delta.dx,\n                curve: Curves.linear,\n              );\n            }\n          },\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/hive_registrar.g.dart",
    "content": "// Generated by Hive CE\n// Do not modify\n// Check in to version control\n\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_tag.dart';\nimport 'package:kazumi/modules/collect/collect_change_module.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/modules/history/history_module.dart';\nimport 'package:kazumi/modules/search/search_history_module.dart';\n\nextension HiveRegistrar on HiveInterface {\n  void registerAdapters() {\n    registerAdapter(BangumiItemAdapter());\n    registerAdapter(BangumiTagAdapter());\n    registerAdapter(CollectedBangumiAdapter());\n    registerAdapter(CollectedBangumiChangeAdapter());\n    registerAdapter(DownloadEpisodeAdapter());\n    registerAdapter(DownloadRecordAdapter());\n    registerAdapter(HistoryAdapter());\n    registerAdapter(ProgressAdapter());\n    registerAdapter(SearchHistoryAdapter());\n  }\n}\n\nextension IsolatedHiveRegistrar on IsolatedHiveInterface {\n  void registerAdapters() {\n    registerAdapter(BangumiItemAdapter());\n    registerAdapter(BangumiTagAdapter());\n    registerAdapter(CollectedBangumiAdapter());\n    registerAdapter(CollectedBangumiChangeAdapter());\n    registerAdapter(DownloadEpisodeAdapter());\n    registerAdapter(DownloadRecordAdapter());\n    registerAdapter(HistoryAdapter());\n    registerAdapter(ProgressAdapter());\n    registerAdapter(SearchHistoryAdapter());\n  }\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/app_module.dart';\nimport 'package:kazumi/app_widget.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/settings/theme_provider.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:hive_ce_flutter/hive_flutter.dart';\nimport 'package:kazumi/request/request.dart';\nimport 'package:kazumi/utils/proxy_manager.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:kazumi/pages/error/storage_error_page.dart';\nimport 'package:provider/provider.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  MediaKit.ensureInitialized();\n  if (Platform.isAndroid || Platform.isIOS) {\n    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(\n      systemNavigationBarColor: Colors.transparent,\n      systemNavigationBarDividerColor: Colors.transparent,\n      statusBarColor: Colors.transparent,\n    ));\n  }\n\n  if (Platform.isAndroid) {\n    await Utils.checkWebViewFeatureSupport();\n  }\n\n  try {\n    final hivePath = '${(await getApplicationSupportDirectory()).path}/hive';\n    await Hive.initFlutter(hivePath);\n    await GStorage.init();\n  } catch (e) {\n    // Log the error for debugging (if logger is available)\n    debugPrint('Storage initialization failed: $e');\n\n    if (Platform.isWindows) {\n      await windowManager.ensureInitialized();\n      windowManager.waitUntilReadyToShow(null, () async {\n        // Native window show has been blocked in `flutter_windows.cppL36` to avoid flickering.\n        // Without this. the window will never show on Windows.\n        await windowManager.show();\n        await windowManager.focus();\n      });\n    }\n    runApp(MaterialApp(\n        title: '初始化失败',\n        localizationsDelegates: GlobalMaterialLocalizations.delegates,\n        supportedLocales: const [\n          Locale.fromSubtags(\n              languageCode: 'zh', scriptCode: 'Hans', countryCode: \"CN\")\n        ],\n        locale: const Locale.fromSubtags(\n            languageCode: 'zh', scriptCode: 'Hans', countryCode: \"CN\"),\n        builder: (context, child) {\n          return const StorageErrorPage();\n        }));\n    return;\n  }\n  bool showWindowButton = await GStorage.setting\n      .get(SettingBoxKey.showWindowButton, defaultValue: false);\n  if (Utils.isDesktop()) {\n    await windowManager.ensureInitialized();\n    bool isLowResolution = await Utils.isLowResolution();\n    WindowOptions windowOptions = WindowOptions(\n      size: isLowResolution ? const Size(840, 600) : const Size(1280, 860),\n      center: true,\n      skipTaskbar: false,\n      // macOS always hide title bar regardless of showWindowButton setting\n      titleBarStyle: (Platform.isMacOS || !showWindowButton)\n          ? TitleBarStyle.hidden\n          : TitleBarStyle.normal,\n      windowButtonVisibility: showWindowButton,\n      title: 'Kazumi',\n    );\n    windowManager.waitUntilReadyToShow(windowOptions, () async {\n      // Native window show has been blocked in `flutter_windows.cppL36` to avoid flickering.\n      // Without this. the window will never show on Windows.\n      await windowManager.show();\n      await windowManager.focus();\n    });\n  }\n  Request();\n  await Request.setCookie();\n  ProxyManager.applyProxy();\n  runApp(\n    ChangeNotifierProvider(\n      create: (_) => ThemeProvider(),\n      child: ModularApp(\n        module: AppModule(),\n        child: const AppWidget(),\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "lib/modules/bangumi/bangumi_item.dart",
    "content": "import 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_tag.dart';\n\npart 'bangumi_item.g.dart';\n\n@HiveType(typeId: 0)\nclass BangumiItem {\n  @HiveField(0)\n  int id;\n  @HiveField(1)\n  int type;\n  @HiveField(2)\n  String name;\n  @HiveField(3)\n  String nameCn;\n  @HiveField(4)\n  String summary;\n  @HiveField(5)\n  String airDate;\n  @HiveField(6)\n  int airWeekday;\n  @HiveField(7)\n  int rank;\n  @HiveField(8)\n  Map<String, String> images;\n  @HiveField(9, defaultValue: [])\n  List<BangumiTag> tags;\n  @HiveField(10, defaultValue: [])\n  List<String> alias;\n  @HiveField(11, defaultValue: 0.0)\n  double ratingScore;\n  @HiveField(12, defaultValue: 0)\n  int votes;\n  @HiveField(13, defaultValue: [])\n  List<int> votesCount;\n  @HiveField(14, defaultValue: '')\n  String info;\n\n  BangumiItem({\n    required this.id,\n    required this.type,\n    required this.name,\n    required this.nameCn,\n    required this.summary,\n    required this.airDate,\n    required this.airWeekday,\n    required this.rank,\n    required this.images,\n    required this.tags,\n    required this.alias,\n    required this.ratingScore,\n    required this.votes,\n    required this.votesCount,\n    required this.info,\n  });\n\n  factory BangumiItem.fromJson(Map<String, dynamic> json) {\n    List<String> parseBangumiAliases(Map<String, dynamic> jsonData) {\n      if (jsonData.containsKey('infobox') && jsonData['infobox'] is List) {\n        final List<dynamic> infobox = jsonData['infobox'];\n        for (var item in infobox) {\n          if (item is Map<String, dynamic> && item['key'] == '别名') {\n            final dynamic value = item['value'];\n            if (value is List) {\n              return value\n                  .map<String>((element) {\n                    if (element is Map<String, dynamic> &&\n                        element.containsKey('v')) {\n                      return element['v'].toString();\n                    }\n                    return '';\n                  })\n                  .where((alias) => alias.isNotEmpty)\n                  .toList();\n            }\n          }\n        }\n      }\n      return [];\n    }\n\n    List<int> parseBangumiVoteCount(Map<String, dynamic> jsonData) {\n      if (!jsonData.containsKey('rating')) {\n        return [];\n      }\n      final json = jsonData['rating']['count'];\n      // For api.bgm.tv\n      if (json is Map<String, dynamic>) {\n        return List<int>.generate(10, (i) => json['${i+1}'] as int);\n      }\n      // For next.bgm.tv\n      if (json is List<dynamic>) {\n        return json.map((e) => e as int).toList();\n      }\n      return [];\n    }\n\n    List list = json['tags'] ?? [];\n    List<String> bangumiAlias = parseBangumiAliases(json);\n    List<BangumiTag> tagList = list.map((i) => BangumiTag.fromJson(i)).toList();\n    List<int> voteList = parseBangumiVoteCount(json);\n    return BangumiItem(\n      id: json['id'],\n      type: json['type'] ?? 2,\n      name: json['name'] ?? '',\n      nameCn: (json['name_cn'] ?? '') == ''\n          ? (((json['nameCN'] ?? '') == '') ? json['name'] : json['nameCN'])\n          : json['name_cn'],\n      summary: json['summary'] ?? '',\n      airDate: json['date'] ?? '',\n      airWeekday: Utils.dateStringToWeekday(json['date'] ?? '2000-11-11'),\n      rank: json['rating']['rank'] ?? 0,\n      images: Map<String, String>.from(\n        json['images'] ??\n            {\n              \"large\": json['image'],\n              \"common\": \"\",\n              \"medium\": \"\",\n              \"small\": \"\",\n              \"grid\": \"\"\n            },\n      ),\n      tags: tagList,\n      alias: bangumiAlias,\n      ratingScore: double.parse(\n          (json['rating']['score'] ?? 0.0).toDouble().toStringAsFixed(1)),\n      votes: json['rating']['total'] ?? 0,\n      votesCount: voteList,\n      info: json['info'] ?? '',\n    );\n  }\n}\n"
  },
  {
    "path": "lib/modules/bangumi/bangumi_item.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'bangumi_item.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass BangumiItemAdapter extends TypeAdapter<BangumiItem> {\n  @override\n  final typeId = 0;\n\n  @override\n  BangumiItem read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return BangumiItem(\n      id: (fields[0] as num).toInt(),\n      type: (fields[1] as num).toInt(),\n      name: fields[2] as String,\n      nameCn: fields[3] as String,\n      summary: fields[4] as String,\n      airDate: fields[5] as String,\n      airWeekday: (fields[6] as num).toInt(),\n      rank: (fields[7] as num).toInt(),\n      images: (fields[8] as Map).cast<String, String>(),\n      tags: fields[9] == null ? [] : (fields[9] as List).cast<BangumiTag>(),\n      alias: fields[10] == null ? [] : (fields[10] as List).cast<String>(),\n      ratingScore: fields[11] == null ? 0.0 : (fields[11] as num).toDouble(),\n      votes: fields[12] == null ? 0 : (fields[12] as num).toInt(),\n      votesCount: fields[13] == null ? [] : (fields[13] as List).cast<int>(),\n      info: fields[14] == null ? '' : fields[14] as String,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, BangumiItem obj) {\n    writer\n      ..writeByte(15)\n      ..writeByte(0)\n      ..write(obj.id)\n      ..writeByte(1)\n      ..write(obj.type)\n      ..writeByte(2)\n      ..write(obj.name)\n      ..writeByte(3)\n      ..write(obj.nameCn)\n      ..writeByte(4)\n      ..write(obj.summary)\n      ..writeByte(5)\n      ..write(obj.airDate)\n      ..writeByte(6)\n      ..write(obj.airWeekday)\n      ..writeByte(7)\n      ..write(obj.rank)\n      ..writeByte(8)\n      ..write(obj.images)\n      ..writeByte(9)\n      ..write(obj.tags)\n      ..writeByte(10)\n      ..write(obj.alias)\n      ..writeByte(11)\n      ..write(obj.ratingScore)\n      ..writeByte(12)\n      ..write(obj.votes)\n      ..writeByte(13)\n      ..write(obj.votesCount)\n      ..writeByte(14)\n      ..write(obj.info);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is BangumiItemAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/bangumi/bangumi_tag.dart",
    "content": "import 'package:hive_ce/hive.dart';\n\npart 'bangumi_tag.g.dart';\n\n@HiveType(typeId: 4)\nclass BangumiTag {\n  @HiveField(0)\n  final String name;\n  @HiveField(1)\n  final int count;\n  @HiveField(2)\n  final int totalCount;\n\n  BangumiTag({\n    required this.name,\n    required this.count,\n    required this.totalCount,\n  });\n\n  factory BangumiTag.fromJson(Map<String, dynamic> json) {\n    return BangumiTag(\n      name: json['name'] ?? '',\n      count: json['count'] ?? 0,\n      totalCount: json['total_cont'] ?? 0,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'name': name,\n      'count': count,\n      'total_cont': totalCount,\n    };\n  }\n}"
  },
  {
    "path": "lib/modules/bangumi/bangumi_tag.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'bangumi_tag.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass BangumiTagAdapter extends TypeAdapter<BangumiTag> {\n  @override\n  final typeId = 4;\n\n  @override\n  BangumiTag read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return BangumiTag(\n      name: fields[0] as String,\n      count: (fields[1] as num).toInt(),\n      totalCount: (fields[2] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, BangumiTag obj) {\n    writer\n      ..writeByte(3)\n      ..writeByte(0)\n      ..write(obj.name)\n      ..writeByte(1)\n      ..write(obj.count)\n      ..writeByte(2)\n      ..write(obj.totalCount);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is BangumiTagAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/bangumi/episode_item.dart",
    "content": "class EpisodeInfo {\n  int id;\n  num episode;\n  int type;\n  String name;\n  String nameCn;\n\n  EpisodeInfo({\n    required this.id,\n    required this.episode,\n    required this.type,\n    required this.name,\n    required this.nameCn,\n  });\n\n  factory EpisodeInfo.fromJson(Map<String, dynamic> json) {\n    return EpisodeInfo(\n        id: json['id'] ?? 0,\n        episode: json['sort'] ?? 0,\n        type: json['type'] ?? 0,\n        name: json['name'] ?? '',\n        nameCn: json['name_cn'] ?? '');\n  }\n\n  factory EpisodeInfo.fromTemplate() {\n    return EpisodeInfo(id: 0, episode: 0, type: 0, name: '', nameCn: '');\n  }\n\n  void reset() {\n    id = 0;\n    episode = 0;\n    type = 0;\n    name = '';\n    nameCn = '';\n  }\n\n  String readType() {\n    switch (type) {\n      case 0:\n        return 'ep';\n      case 1:\n        return 'sp';\n      case 2:\n        return 'op';\n      case 3:\n        return 'ed';\n      default:\n        return '';\n    }\n  }\n}\n"
  },
  {
    "path": "lib/modules/bangumi/weekday_item.dart",
    "content": "class Weekday {\n  String? en;\n  String? cn;\n  String? ja;\n  int? id;\n\n  Weekday({this.en, this.cn, this.ja, this.id});\n\n  factory Weekday.fromJson(Map<String, dynamic> json) {\n    return Weekday(\n      en: json['en'],\n      cn: json['cn'],\n      ja: json['ja'],\n      id: json['id'],\n    );\n  }\n}\n\n\n"
  },
  {
    "path": "lib/modules/character/character_full_item.dart",
    "content": "class CharacterFullItem {\n  final int id;\n  final String name;\n  final String nameCN;\n  final String info;\n  final String summary;\n  final String image;\n\n  CharacterFullItem({\n    required this.id,\n    required this.name,\n    required this.nameCN,\n    required this.info,\n    required this.summary,\n    required this.image,\n  });\n\n  factory CharacterFullItem.fromJson(Map<String, dynamic> json) {\n    return CharacterFullItem(\n      id: json['id'] ?? 0,\n      name: json['name'] ?? '',\n      nameCN: json['nameCN'] ?? '',\n      info: json['info'] ?? '',\n      summary: json['summary'] ?? '',\n      image: json['images']['large'] ?? '',\n    );\n  }\n\n  factory CharacterFullItem.fromTemplate() {\n    return CharacterFullItem(\n      id: 0,\n      name: '',\n      nameCN: '',\n      info: '',\n      summary: '',\n      image: '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'name': name,\n      'nameCN': nameCN,\n      'info': info,\n      'summary': summary,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/characters/actor_item.dart",
    "content": "class ActorAvator {\n  final String small;\n  final String medium;\n  final String grid;\n  final String large;\n\n  ActorAvator({\n    required this.small,\n    required this.medium,\n    required this.grid,\n    required this.large,\n  });\n\n  factory ActorAvator.fromJson(Map<String, dynamic> json) {\n    return ActorAvator(\n      small: json['small'] ?? '',\n      medium: json['medium'] ?? '',\n      grid: json['grid'] ?? '',\n      large: json['large'] ?? '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'small': small,\n      'medium': medium,\n      'grid': grid,\n      'large': large,\n    };\n  }\n}\n\nclass ActorItem {\n  final int id;\n  final int type;\n  final String name;\n  final ActorAvator avator;\n\n  ActorItem({\n    required this.id,\n    required this.type,\n    required this.name,\n    required this.avator,\n  });\n\n  factory ActorItem.fromJson(Map<String, dynamic> json) {\n    return ActorItem(\n      id: json['id'] ?? 0,\n      type: json['type'] ?? 0,\n      name: json['name'] ?? '',\n      avator: ActorAvator.fromJson(json['images'] as Map<String, dynamic>),\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'type': type,\n      'name': name,\n      'images': avator.toJson(),\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/characters/character_item.dart",
    "content": "import 'package:kazumi/modules/characters/actor_item.dart';\n\nclass CharacterAvator {\n  final String small;\n  final String medium;\n  final String grid;\n  final String large;\n\n  CharacterAvator({\n    required this.small,\n    required this.medium,\n    required this.grid,\n    required this.large,\n  });\n\n  factory CharacterAvator.fromJson(Map<String, dynamic> json) {\n    return CharacterAvator(\n      small: json['small'] ?? '',\n      medium: json['medium'] ?? '',\n      grid: json['grid'] ?? '',\n      large: json['large'] ?? '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'small': small,\n      'medium': medium,\n      'grid': grid,\n      'large': large,\n    };\n  }\n}\n\nclass CharacterExtraInfo {\n  String nameCn;\n  String summary;\n\n  CharacterExtraInfo({required this.nameCn, required this.summary});\n\n  factory CharacterExtraInfo.fromJson(Map<String, dynamic> json) {\n    String nameCn = '';\n    final String hasNameCn = json['infobox'][0]['key'];\n    if (hasNameCn == '简体中文名') {\n      nameCn = json['infobox'][0]['value'];\n    }\n    return CharacterExtraInfo(\n      nameCn: nameCn,\n      summary: json['summary']\n    );\n  }\n}\n\nclass CharacterItem {\n  final int id;\n  final int type;\n  final String name;\n  final String relation;\n  final CharacterAvator avator;\n  final List<ActorItem> actorList;\n  CharacterExtraInfo info;\n\n  CharacterItem({\n    required this.id,\n    required this.type,\n    required this.name,\n    required this.relation,\n    required this.avator,\n    required this.actorList,\n    required this.info\n  });\n\n  factory CharacterItem.fromJson(Map<String, dynamic> json) {\n    var list = json['actors'] as List;\n    List<ActorItem> resActorList =\n        list.map((i) => ActorItem.fromJson(i)).toList();\n    return CharacterItem(\n      id: json['id'] ?? 0,\n      type: json['type'] ?? 0,\n      name: json['name'] ?? '',\n      relation: json['relation'] ?? '未知',\n      avator: CharacterAvator.fromJson(json['images'] as Map<String, dynamic>),\n      actorList: resActorList,\n      info: CharacterExtraInfo(nameCn: '', summary: '')\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'type': type,\n      'name': name,\n      'relation': relation,\n      'images': avator.toJson(),\n      'actors': actorList.map((e) => e.toJson()).toList(),\n    };\n  }\n}"
  },
  {
    "path": "lib/modules/characters/characters_response.dart",
    "content": "import 'package:kazumi/request/api.dart';\nimport 'package:kazumi/modules/characters/character_item.dart';\n\n/// The response from [Api.bangumiInfoByID]\n/// It contains a list of [CharacterItem]\n/// It is used to show general information about seraval bangumi characters\nclass CharactersResponse {\n  final List<CharacterItem> charactersList;\n\n  CharactersResponse({\n    required this.charactersList,\n  });\n\n  factory CharactersResponse.fromJson(List list) {\n    List<CharacterItem> resCharactersList =\n        list.map((i) => CharacterItem.fromJson(i)).toList();\n    return CharactersResponse(\n      charactersList: resCharactersList,\n    );\n  }\n\n  factory CharactersResponse.fromTemplate() {\n    return CharactersResponse(\n      charactersList: [],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'charactersList': charactersList.map((e) => e.toJson()).toList(),\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/collect/collect_change_module.dart",
    "content": "import 'package:hive_ce/hive.dart';\n\npart 'collect_change_module.g.dart';\n\n// The box stores the changes history of collected bangumi\n// The changes will be used to sync with webDav\n@HiveType(typeId: 5)\nclass CollectedBangumiChange {\n  // timestamp in seconds\n  // hivebox has limited the length of key, the max number is 4294967295\n  // we have to use timestamp in seconds as key to avoid key conflict and hive key limit\n  @HiveField(0)\n  int id;\n\n  @HiveField(1)\n  int bangumiID;\n\n  // 1. add\n  // 2. update\n  // 3. delete\n  @HiveField(2)\n  int action;\n\n  // 1. 在看\n  // 2. 想看\n  // 3. 搁置\n  // 4. 看过\n  // 5. 抛弃\n  @HiveField(3)\n  int type;\n\n\n  @HiveField(4)\n  int timestamp;\n\n  CollectedBangumiChange(this.id, this.bangumiID, this.action,this.type, this.timestamp);\n\n  @override\n  String toString() {\n    return 'id: $id, bangumi: $bangumiID, action: $action, time: $timestamp';\n  }\n}\n"
  },
  {
    "path": "lib/modules/collect/collect_change_module.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'collect_change_module.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass CollectedBangumiChangeAdapter\n    extends TypeAdapter<CollectedBangumiChange> {\n  @override\n  final typeId = 5;\n\n  @override\n  CollectedBangumiChange read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return CollectedBangumiChange(\n      (fields[0] as num).toInt(),\n      (fields[1] as num).toInt(),\n      (fields[2] as num).toInt(),\n      (fields[3] as num).toInt(),\n      (fields[4] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, CollectedBangumiChange obj) {\n    writer\n      ..writeByte(5)\n      ..writeByte(0)\n      ..write(obj.id)\n      ..writeByte(1)\n      ..write(obj.bangumiID)\n      ..writeByte(2)\n      ..write(obj.action)\n      ..writeByte(3)\n      ..write(obj.type)\n      ..writeByte(4)\n      ..write(obj.timestamp);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is CollectedBangumiChangeAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/collect/collect_module.dart",
    "content": "import 'package:hive_ce/hive.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\n\npart 'collect_module.g.dart';\n\n@HiveType(typeId: 3)\nclass CollectedBangumi {\n  @HiveField(0)\n  BangumiItem bangumiItem;\n\n  @HiveField(1)\n  DateTime time;\n\n  // 1. 在看\n  // 2. 想看\n  // 3. 搁置\n  // 4. 看过\n  // 5. 抛弃\n  @HiveField(2)\n  int type;\n\n  String get key => bangumiItem.id.toString();\n\n  CollectedBangumi(this.bangumiItem, this.time, this.type);\n\n  static String getKey(BangumiItem bangumiItem) => bangumiItem.id.toString();\n\n  @override\n  String toString() {\n    return 'type: $type, time: $time, anime: ${bangumiItem.name}';\n  }\n}\n"
  },
  {
    "path": "lib/modules/collect/collect_module.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'collect_module.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass CollectedBangumiAdapter extends TypeAdapter<CollectedBangumi> {\n  @override\n  final typeId = 3;\n\n  @override\n  CollectedBangumi read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return CollectedBangumi(\n      fields[0] as BangumiItem,\n      fields[1] as DateTime,\n      (fields[2] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, CollectedBangumi obj) {\n    writer\n      ..writeByte(3)\n      ..writeByte(0)\n      ..write(obj.bangumiItem)\n      ..writeByte(1)\n      ..write(obj.time)\n      ..writeByte(2)\n      ..write(obj.type);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is CollectedBangumiAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/collect/collect_type.dart",
    "content": "/// 收藏类型枚举\n///\n/// 用于标识番剧的收藏状态\nenum CollectType {\n  /// 未收藏\n  none(0, '未收藏'),\n\n  /// 在看\n  watching(1, '在看'),\n\n  /// 想看\n  planToWatch(2, '想看'),\n\n  /// 搁置\n  onHold(3, '搁置'),\n\n  /// 看过\n  watched(4, '看过'),\n\n  /// 抛弃\n  abandoned(5, '抛弃');\n\n  const CollectType(this.value, this.label);\n\n  /// 数值表示\n  final int value;\n\n  /// 显示标签\n  final String label;\n\n  /// 根据数值获取枚举\n  static CollectType fromValue(int value) {\n    return CollectType.values.firstWhere(\n      (type) => type.value == value,\n      orElse: () => CollectType.none,\n    );\n  }\n\n  /// 是否为有效的收藏状态（排除未收藏）\n  bool get isCollected => this != CollectType.none;\n}\n"
  },
  {
    "path": "lib/modules/comments/comment_item.dart",
    "content": "class UserAvatar {\n  final String small;\n  final String medium;\n  final String large;\n\n  UserAvatar({\n    required this.small,\n    required this.medium,\n    required this.large,\n  });\n\n  factory UserAvatar.fromJson(Map<String, dynamic> json) {\n    return UserAvatar(\n      small: json['small'] ?? '',\n      medium: json['medium'] ?? '',\n      large: json['large'] ?? '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'small': small,\n      'medium': medium,\n      'large': large,\n    };\n  }\n}\n\nclass User {\n  final int id;\n  final String username;\n  final String nickname;\n  final UserAvatar avatar;\n  final String sign;\n  final int joinedAt;\n\n  User({\n    required this.id,\n    required this.username,\n    required this.nickname,\n    required this.avatar,\n    required this.sign,\n    required this.joinedAt,\n  });\n\n  factory User.fromJson(Map<String, dynamic> json) {\n    return User(\n      id: json['id'] ?? 0,\n      username: json['username'] ?? '',\n      nickname: json['nickname'] ?? '',\n      avatar: UserAvatar.fromJson(json['avatar'] as Map<String, dynamic>),\n      sign: json['sign'] ?? '',\n      joinedAt: json['joinedAt'] ?? 0,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'username': username,\n      'nickname': nickname,\n      'avatar': avatar.toJson(),\n      'sign': sign,\n      'joinedAt': joinedAt,\n    };\n  }\n}\n\nclass Comment {\n  final int rate;\n  final String comment;\n  final int updatedAt;\n\n  Comment({\n    required this.rate,\n    required this.comment,\n    required this.updatedAt,\n  });\n\n\n  factory Comment.fromJson(Map<String, dynamic> json) {\n    return Comment(\n      rate: json['rate'] ?? 0,\n      comment: json['comment'] ?? '',\n      updatedAt: json['updatedAt'] ?? 0,\n    );\n  }\n\n\n  Map<String, dynamic> toJson() {\n    return {\n      'rate': rate,\n      'comment': comment,\n      'updatedAt': updatedAt,\n    };\n  }\n}\n\nclass CommentItem {\n  final User user;\n  final Comment comment;\n\n  CommentItem({\n    required this.user,\n    required this.comment,\n  });\n\n  factory CommentItem.fromJson(Map<String, dynamic> json) {\n    return CommentItem(\n      user: User.fromJson(json['user']),\n      comment: Comment.fromJson(json),\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'user': user.toJson(),\n      'comment': comment.toJson(),\n    };\n  }\n}\n\nclass EpisodeComment {\n  final User user;\n  final String comment;\n  final int createdAt;\n\n  EpisodeComment({\n    required this.user,\n    required this.comment,\n    required this.createdAt,\n  });\n\n\n  factory EpisodeComment.fromJson(Map<String, dynamic> json) {\n    return EpisodeComment(\n      user: User.fromJson(json['user']),\n      comment: json['content'] ?? '',\n      createdAt: json['createdAt'] ?? 0,\n    );\n  }\n\n\n  Map<String, dynamic> toJson() {\n    return {\n      'user': user.toJson(),\n      'content': comment,\n      'createdAt': createdAt,\n    };\n  }\n}\n\nclass EpisodeCommentItem {\n  final EpisodeComment comment;\n  final List<EpisodeComment> replies;\n\n  EpisodeCommentItem({\n    required this.comment,\n    required this.replies\n  });\n\n  factory EpisodeCommentItem.fromJson(Map<String, dynamic> json) {\n    var list = json['replies'] as List;\n    List<EpisodeComment> tempList =\n        list.map((i) => EpisodeComment.fromJson(i)).toList();\n    return EpisodeCommentItem(\n      comment: EpisodeComment.fromJson(json),\n      replies: tempList\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'comment': comment.toJson(),\n      'list': replies,\n    };\n  }\n}\n\nclass CharacterComment {\n  final User user;\n  final String comment;\n  final int createdAt;\n\n  CharacterComment({\n    required this.user,\n    required this.comment,\n    required this.createdAt,\n  });\n\n\n  factory CharacterComment.fromJson(Map<String, dynamic> json) {\n    return CharacterComment(\n      user: User.fromJson(json['user']),\n      comment: json['content'] ?? '',\n      createdAt: json['createdAt'] ?? 0,\n    );\n  }\n\n\n  Map<String, dynamic> toJson() {\n    return {\n      'user': user.toJson(),\n      'content': comment,\n      'createdAt': createdAt,\n    };\n  }\n}\n\nclass CharacterCommentItem {\n  final CharacterComment comment;\n  final List<CharacterComment> replies;\n\n  CharacterCommentItem({\n    required this.comment,\n    required this.replies\n  });\n\n  factory CharacterCommentItem.fromJson(Map<String, dynamic> json) {\n    var list = json['replies'] as List;\n    List<CharacterComment> tempList =\n        list.map((i) => CharacterComment.fromJson(i)).toList();\n    return CharacterCommentItem(\n      comment: CharacterComment.fromJson(json),\n      replies: tempList\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'comment': comment.toJson(),\n      'list': replies,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/comments/comment_response.dart",
    "content": "import 'package:kazumi/modules/comments/comment_item.dart';\n\nclass CommentResponse {\n  List<CommentItem> commentList;\n  int total;\n\n  CommentResponse({\n    required this.commentList,\n    required this.total,\n  });\n\n  factory CommentResponse.fromJson(Map<String, dynamic> json) {\n    List? list = (json['list'] as List?) ?? (json['data'] as List?);\n    List<CommentItem>? resCommentList =\n        list?.map((i) => CommentItem.fromJson(i)).toList();\n    return CommentResponse(\n      commentList: resCommentList ?? <CommentItem>[],\n      total: json['total'],\n    );\n  }\n\n  factory CommentResponse.fromTemplate() {\n    return CommentResponse(\n      commentList: [],\n      total: 0,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'list': commentList,\n      'total': total,\n    };\n  }\n}\n\nclass EpisodeCommentResponse {\n  List<EpisodeCommentItem> commentList;\n\n  EpisodeCommentResponse({\n    required this.commentList,\n  });\n\n  factory EpisodeCommentResponse.fromJson(List<dynamic> json) {\n    List<EpisodeCommentItem>? resCommentList =\n        (json as List?)?.map((i) => EpisodeCommentItem.fromJson(i)).toList();\n    return EpisodeCommentResponse(\n      commentList: resCommentList ?? <EpisodeCommentItem>[],\n    );\n  }\n\n  factory EpisodeCommentResponse.fromTemplate() {\n    return EpisodeCommentResponse(\n      commentList: [],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'list': commentList,\n    };\n  }\n}\n\nclass CharacterCommentResponse {\n  List<CharacterCommentItem> commentList;\n\n  CharacterCommentResponse({\n    required this.commentList,\n  });\n\n  factory CharacterCommentResponse.fromJson(List<dynamic> json) {\n    List<CharacterCommentItem>? resCommentList =\n        (json as List?)?.map((i) => CharacterCommentItem.fromJson(i)).toList();\n    return CharacterCommentResponse(\n      commentList: resCommentList ?? <CharacterCommentItem>[],\n    );\n  }\n\n  factory CharacterCommentResponse.fromTemplate() {\n    return CharacterCommentResponse(\n      commentList: [],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'list': commentList,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/danmaku/danmaku_episode_response.dart",
    "content": "class DanmakuEpisode {\n  int episodeId;\n  String episodeTitle;\n\n  DanmakuEpisode({\n    required this.episodeId,\n    required this.episodeTitle,\n  });\n\n  factory DanmakuEpisode.fromJson(Map<String, dynamic> json) {\n    return DanmakuEpisode(\n      episodeId: json['episodeId'],\n      episodeTitle: json['episodeTitle'],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'episodeId': episodeId,\n      'episodeTitle': episodeTitle,\n    };\n  }\n}\n\nclass DanmakuEpisodeResponse {\n  int bangumiId;\n  List<DanmakuEpisode> episodes;\n  int errorCode;\n  bool success;\n  String errorMessage;\n\n  DanmakuEpisodeResponse({\n    required this.bangumiId,\n    required this.episodes,\n    required this.errorCode,\n    required this.success,\n    required this.errorMessage,\n  });\n\n  factory DanmakuEpisodeResponse.fromJson(Map<String, dynamic> json) {\n    var list = json['bangumi']['episodes'] as List;\n    List<DanmakuEpisode> episodeList =\n        list.map((i) => DanmakuEpisode.fromJson(i)).toList();\n\n    return DanmakuEpisodeResponse(\n      bangumiId: json['bangumi']['animeId'],\n      episodes: episodeList,\n      errorCode: json['errorCode'],\n      success: json['success'],\n      errorMessage: json['errorMessage'],\n    );\n  }\n\n  factory DanmakuEpisodeResponse.fromTemplate() {\n    return DanmakuEpisodeResponse(\n      bangumiId: 0,\n      episodes: [],\n      errorCode: 0,\n      success: false,\n      errorMessage: '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'bangumi': episodes.map((episode) => episode.toJson()).toList(),\n      'errorCode': errorCode,\n      'success': success,\n      'errorMessage': errorMessage,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/modules/danmaku/danmaku_module.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/utils/utils.dart';\n\nclass Danmaku {\n  // 弹幕内容\n  String message;\n  // 弹幕时间\n  double time;\n  // 弹幕类型 (1-普通弹幕，4-底部弹幕，5-顶部弹幕)\n  int type;\n  // 弹幕颜色\n  Color color;\n  // 弹幕来源 ([BiliBili], [Gamer])\n  String source;\n\n  Danmaku({required this.message, required this.time, required this.type, required this.color, required this.source});\n\n  factory Danmaku.fromJson(Map<String, dynamic> json) {\n    String messageValue = json['m'];\n    List<String> parts = json['p'].split(',');\n    double timeValue = double.parse(parts[0]);\n    int typeValue = int.parse(parts[1]);\n    Color color = Utils.generateDanmakuColor(int.parse(parts[2]));\n    String sourceValue = parts[3];\n    return Danmaku(time: timeValue, message: messageValue, type: typeValue, color: color, source: sourceValue);\n  }\n\n  /// 序列化为 JSON 格式 (与 fromJson 格式一致)\n  Map<String, dynamic> toJson() {\n    // 只存储 RGB 部分 (与 DanDanPlay API 格式一致)\n    final colorValue = ((color.r * 255).toInt() << 16) |\n                       ((color.g * 255).toInt() << 8) |\n                       (color.b * 255).toInt();\n    return {\n      'm': message,\n      'p': '$time,$type,$colorValue,$source',\n    };\n  }\n}"
  },
  {
    "path": "lib/modules/danmaku/danmaku_search_response.dart",
    "content": "class DanmakuAnime {\n  int animeId;\n  String animeTitle;\n  String type;\n  String typeDescription;\n  String imageUrl;\n  DateTime startDate;\n  int episodeCount;\n  double rating;\n  bool isFavorited;\n\n  DanmakuAnime({\n    required this.animeId,\n    required this.animeTitle,\n    required this.type,\n    required this.typeDescription,\n    required this.imageUrl,\n    required this.startDate,\n    required this.episodeCount,\n    required this.rating,\n    required this.isFavorited,\n  });\n\n  factory DanmakuAnime.fromJson(Map<String, dynamic> json) {\n    return DanmakuAnime(\n      animeId: json['animeId'],\n      animeTitle: json['animeTitle'],\n      type: json['type'],\n      typeDescription: json['typeDescription'],\n      imageUrl: json['imageUrl'],\n      startDate: DateTime.parse(json['startDate']),\n      episodeCount: json['episodeCount'],\n      rating: json['rating'].toDouble(),\n      isFavorited: json['isFavorited'],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'animeId': animeId,\n      'animeTitle': animeTitle,\n      'type': type,\n      'typeDescription': typeDescription,\n      'imageUrl': imageUrl,\n      'startDate': startDate.toIso8601String(),\n      'episodeCount': episodeCount,\n      'rating': rating,\n      'isFavorited': isFavorited,\n    };\n  }\n}\n\nclass DanmakuSearchResponse {\n  List<DanmakuAnime> animes;\n  int errorCode;\n  bool success;\n  String errorMessage;\n\n  DanmakuSearchResponse({\n    required this.animes,\n    required this.errorCode,\n    required this.success,\n    required this.errorMessage,\n  });\n\n  factory DanmakuSearchResponse.fromJson(Map<String, dynamic> json) {\n    var list = json['animes'] as List;\n    List<DanmakuAnime> animeList = list.map((i) => DanmakuAnime.fromJson(i)).toList();\n\n    return DanmakuSearchResponse(\n      animes: animeList,\n      errorCode: json['errorCode'],\n      success: json['success'],\n      errorMessage: json['errorMessage'],\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'animes': animes.map((anime) => anime.toJson()).toList(),\n      'errorCode': errorCode,\n      'success': success,\n      'errorMessage': errorMessage,\n    };\n  }\n}"
  },
  {
    "path": "lib/modules/download/download_module.dart",
    "content": "import 'package:hive_ce/hive.dart';\n\npart 'download_module.g.dart';\n\n@HiveType(typeId: 7)\nclass DownloadRecord {\n  @HiveField(0)\n  int bangumiId;\n\n  @HiveField(1)\n  String bangumiName;\n\n  @HiveField(2)\n  String bangumiCover;\n\n  @HiveField(3)\n  String pluginName;\n\n  @HiveField(4)\n  Map<int, DownloadEpisode> episodes;\n\n  @HiveField(5)\n  DateTime createdAt;\n\n  String get key => '${pluginName}_$bangumiId';\n\n  DownloadRecord(\n    this.bangumiId,\n    this.bangumiName,\n    this.bangumiCover,\n    this.pluginName,\n    this.episodes,\n    this.createdAt,\n  );\n}\n\n@HiveType(typeId: 8)\nclass DownloadEpisode {\n  @HiveField(0)\n  int episodeNumber;\n\n  @HiveField(1)\n  String episodeName;\n\n  @HiveField(2)\n  int road;\n\n  /// 0=pending 1=resolving 2=downloading 3=completed 4=failed 5=paused\n  @HiveField(3)\n  int status;\n\n  @HiveField(4)\n  double progressPercent;\n\n  @HiveField(5)\n  int totalSegments;\n\n  @HiveField(6)\n  int downloadedSegments;\n\n  @HiveField(7)\n  String localM3u8Path;\n\n  @HiveField(8)\n  String downloadDirectory;\n\n  @HiveField(9)\n  String networkM3u8Url;\n\n  @HiveField(10)\n  DateTime? completedAt;\n\n  @HiveField(11, defaultValue: '')\n  String errorMessage;\n\n  @HiveField(12, defaultValue: 0)\n  int totalBytes;\n\n  @HiveField(13, defaultValue: '')\n  String episodePageUrl;\n\n  /// 缓存的弹幕数据 (JSON 字符串格式)\n  @HiveField(14, defaultValue: '')\n  String danmakuData;\n\n  /// DanDanPlay 番剧 ID (用于弹幕查询缓存)\n  @HiveField(15, defaultValue: 0)\n  int danDanBangumiID;\n\n  DownloadEpisode(\n    this.episodeNumber,\n    this.episodeName,\n    this.road,\n    this.status,\n    this.progressPercent,\n    this.totalSegments,\n    this.downloadedSegments,\n    this.localM3u8Path,\n    this.downloadDirectory,\n    this.networkM3u8Url,\n    this.completedAt,\n    this.errorMessage,\n    this.totalBytes,\n    this.episodePageUrl, {\n    this.danmakuData = '',\n    this.danDanBangumiID = 0,\n  });\n}\n\nclass DownloadStatus {\n  static const int pending = 0;\n  static const int resolving = 1;\n  static const int downloading = 2;\n  static const int completed = 3;\n  static const int failed = 4;\n  static const int paused = 5;\n}\n"
  },
  {
    "path": "lib/modules/download/download_module.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'download_module.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass DownloadRecordAdapter extends TypeAdapter<DownloadRecord> {\n  @override\n  final typeId = 7;\n\n  @override\n  DownloadRecord read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return DownloadRecord(\n      (fields[0] as num).toInt(),\n      fields[1] as String,\n      fields[2] as String,\n      fields[3] as String,\n      (fields[4] as Map).cast<int, DownloadEpisode>(),\n      fields[5] as DateTime,\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, DownloadRecord obj) {\n    writer\n      ..writeByte(6)\n      ..writeByte(0)\n      ..write(obj.bangumiId)\n      ..writeByte(1)\n      ..write(obj.bangumiName)\n      ..writeByte(2)\n      ..write(obj.bangumiCover)\n      ..writeByte(3)\n      ..write(obj.pluginName)\n      ..writeByte(4)\n      ..write(obj.episodes)\n      ..writeByte(5)\n      ..write(obj.createdAt);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is DownloadRecordAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass DownloadEpisodeAdapter extends TypeAdapter<DownloadEpisode> {\n  @override\n  final typeId = 8;\n\n  @override\n  DownloadEpisode read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return DownloadEpisode(\n      (fields[0] as num).toInt(),\n      fields[1] as String,\n      (fields[2] as num).toInt(),\n      (fields[3] as num).toInt(),\n      (fields[4] as num).toDouble(),\n      (fields[5] as num).toInt(),\n      (fields[6] as num).toInt(),\n      fields[7] as String,\n      fields[8] as String,\n      fields[9] as String,\n      fields[10] as DateTime?,\n      fields[11] == null ? '' : fields[11] as String,\n      fields[12] == null ? 0 : (fields[12] as num).toInt(),\n      fields[13] == null ? '' : fields[13] as String,\n      danmakuData: fields[14] == null ? '' : fields[14] as String,\n      danDanBangumiID: fields[15] == null ? 0 : (fields[15] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, DownloadEpisode obj) {\n    writer\n      ..writeByte(16)\n      ..writeByte(0)\n      ..write(obj.episodeNumber)\n      ..writeByte(1)\n      ..write(obj.episodeName)\n      ..writeByte(2)\n      ..write(obj.road)\n      ..writeByte(3)\n      ..write(obj.status)\n      ..writeByte(4)\n      ..write(obj.progressPercent)\n      ..writeByte(5)\n      ..write(obj.totalSegments)\n      ..writeByte(6)\n      ..write(obj.downloadedSegments)\n      ..writeByte(7)\n      ..write(obj.localM3u8Path)\n      ..writeByte(8)\n      ..write(obj.downloadDirectory)\n      ..writeByte(9)\n      ..write(obj.networkM3u8Url)\n      ..writeByte(10)\n      ..write(obj.completedAt)\n      ..writeByte(11)\n      ..write(obj.errorMessage)\n      ..writeByte(12)\n      ..write(obj.totalBytes)\n      ..writeByte(13)\n      ..write(obj.episodePageUrl)\n      ..writeByte(14)\n      ..write(obj.danmakuData)\n      ..writeByte(15)\n      ..write(obj.danDanBangumiID);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is DownloadEpisodeAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/history/history_module.dart",
    "content": "import 'package:hive_ce/hive.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\n\npart 'history_module.g.dart';\n\n@HiveType(typeId: 1)\nclass History {\n  @HiveField(0)\n  Map<int, Progress> progresses = {};\n\n  @HiveField(1)\n  int lastWatchEpisode;\n\n  @HiveField(2)\n  String adapterName;\n\n  @HiveField(3)\n  BangumiItem bangumiItem;\n\n  @HiveField(4)\n  DateTime lastWatchTime;\n\n  @HiveField(5)\n  String lastSrc;\n\n  @HiveField(6, defaultValue: '')\n  String lastWatchEpisodeName;\n\n  String get key => adapterName + bangumiItem.id.toString();\n\n  History(\n      this.bangumiItem, this.lastWatchEpisode, this.adapterName, this.lastWatchTime, this.lastSrc, this.lastWatchEpisodeName);\n\n  static String getKey(String n, BangumiItem s) => n + s.id.toString();\n\n  @override\n  String toString() {\n    return 'Adapter: $adapterName, anime: ${bangumiItem.name}';\n  }\n}\n\n@HiveType(typeId: 2)\nclass Progress {\n  @HiveField(0)\n  int episode;\n\n  @HiveField(1)\n  int road;\n\n  @HiveField(2)\n  int _progressInMilli;\n\n  Duration get progress => Duration(milliseconds: _progressInMilli);\n\n  set progress(Duration d) => _progressInMilli = d.inMilliseconds;\n\n  Progress(this.episode, this.road, this._progressInMilli);\n\n  @override\n  String toString() {\n    return 'Episode ${episode.toString()}, progress $progress';\n  }\n}\n"
  },
  {
    "path": "lib/modules/history/history_module.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'history_module.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass HistoryAdapter extends TypeAdapter<History> {\n  @override\n  final typeId = 1;\n\n  @override\n  History read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return History(\n      fields[3] as BangumiItem,\n      (fields[1] as num).toInt(),\n      fields[2] as String,\n      fields[4] as DateTime,\n      fields[5] as String,\n      fields[6] == null ? '' : fields[6] as String,\n    )..progresses = (fields[0] as Map).cast<int, Progress>();\n  }\n\n  @override\n  void write(BinaryWriter writer, History obj) {\n    writer\n      ..writeByte(7)\n      ..writeByte(0)\n      ..write(obj.progresses)\n      ..writeByte(1)\n      ..write(obj.lastWatchEpisode)\n      ..writeByte(2)\n      ..write(obj.adapterName)\n      ..writeByte(3)\n      ..write(obj.bangumiItem)\n      ..writeByte(4)\n      ..write(obj.lastWatchTime)\n      ..writeByte(5)\n      ..write(obj.lastSrc)\n      ..writeByte(6)\n      ..write(obj.lastWatchEpisodeName);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is HistoryAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n\nclass ProgressAdapter extends TypeAdapter<Progress> {\n  @override\n  final typeId = 2;\n\n  @override\n  Progress read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return Progress(\n      (fields[0] as num).toInt(),\n      (fields[1] as num).toInt(),\n      (fields[2] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, Progress obj) {\n    writer\n      ..writeByte(3)\n      ..writeByte(0)\n      ..write(obj.episode)\n      ..writeByte(1)\n      ..write(obj.road)\n      ..writeByte(2)\n      ..write(obj._progressInMilli);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ProgressAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/plugin/plugin_http_module.dart",
    "content": "class PluginHTTPItem {\n  String name;\n  String version;\n  bool useNativePlayer;\n  String author;\n  int lastUpdate;\n  bool antiCrawlerEnabled;\n\n  PluginHTTPItem({\n    required this.name,\n    required this.version,\n    required this.useNativePlayer,\n    required this.author,\n    required this.lastUpdate,\n    this.antiCrawlerEnabled = false,\n  });\n\n  factory PluginHTTPItem.fromJson(Map<String, dynamic> json) {\n    final dynamic rawConfig = json['antiCrawlerConfig'];\n    final bool antiCrawlerEnabled = rawConfig is Map<String, dynamic>\n        ? (rawConfig['enabled'] as bool? ?? false)\n        : (json['antiCrawlerEnabled'] as bool? ?? false);\n    return PluginHTTPItem(\n      name: json['name'],\n      version: json['version'],\n      useNativePlayer: json['useNativePlayer'],\n      author: json['author'],\n      lastUpdate: json['lastUpdate'] ?? 0,\n      antiCrawlerEnabled: antiCrawlerEnabled,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/modules/roads/road_module.dart",
    "content": "class Road {\n  String name;\n  List<String> data;\n  List<String> identifier;\n\n  Road({\n    required this.name,\n    required this.data,\n    required this.identifier,\n  });\n}"
  },
  {
    "path": "lib/modules/search/plugin_search_module.dart",
    "content": "class SearchItem {\n  String name;\n  String src;\n\n  SearchItem({\n    required this.name,\n    required this.src,\n  });\n\n  factory SearchItem.fromJson(Map<String, dynamic> json) {\n    return SearchItem(name: json['name'], src: json['src']);\n  }\n}\n\nclass PluginSearchResponse {\n  String pluginName;\n  List<SearchItem> data;\n\n  PluginSearchResponse({\n    required this.pluginName,\n    required this.data,\n  });\n\n  factory PluginSearchResponse.fromJson(Map<String, dynamic> json) {\n    return PluginSearchResponse(\n      pluginName: json['pluginName'],\n      data: (json['data'] as List)\n          .map((itemJson) => SearchItem.fromJson(itemJson))\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/modules/search/search_history_module.dart",
    "content": "import 'package:hive_ce/hive.dart';\n\npart 'search_history_module.g.dart';\n\n@HiveType(typeId: 6)\nclass SearchHistory {\n  @HiveField(0)\n  String keyword;\n\n  @HiveField(1)\n  int timestamp;\n\n  SearchHistory(this.keyword, this.timestamp);\n\n  String get key => timestamp.toString();\n\n  @override\n  String toString() {\n    return 'Search keyword: $keyword, search time: ${DateTime.fromMillisecondsSinceEpoch(timestamp)}';\n  }\n}"
  },
  {
    "path": "lib/modules/search/search_history_module.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'search_history_module.dart';\n\n// **************************************************************************\n// TypeAdapterGenerator\n// **************************************************************************\n\nclass SearchHistoryAdapter extends TypeAdapter<SearchHistory> {\n  @override\n  final typeId = 6;\n\n  @override\n  SearchHistory read(BinaryReader reader) {\n    final numOfFields = reader.readByte();\n    final fields = <int, dynamic>{\n      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),\n    };\n    return SearchHistory(\n      fields[0] as String,\n      (fields[1] as num).toInt(),\n    );\n  }\n\n  @override\n  void write(BinaryWriter writer, SearchHistory obj) {\n    writer\n      ..writeByte(2)\n      ..writeByte(0)\n      ..write(obj.keyword)\n      ..writeByte(1)\n      ..write(obj.timestamp);\n  }\n\n  @override\n  int get hashCode => typeId.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SearchHistoryAdapter &&\n          runtimeType == other.runtimeType &&\n          typeId == other.typeId;\n}\n"
  },
  {
    "path": "lib/modules/staff/staff_item.dart",
    "content": "class StaffFullItem {\n  final Staff staff;\n  final List<Position> positions;\n\n  StaffFullItem({\n    required this.staff,\n    required this.positions,\n  });\n\n  factory StaffFullItem.fromJson(Map<String, dynamic> json) {\n    return StaffFullItem(\n      staff: json['staff'] != null\n          ? Staff.fromJson(json['staff'] as Map<String, dynamic>)\n          : Staff.fromTemplate(),\n      positions: (json['positions'] as List<dynamic>? ?? [])\n          .map((item) => Position.fromJson(item as Map<String, dynamic>))\n          .toList(),\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'staff': staff.toJson(),\n      'positions': positions.map((item) => item.toJson()).toList(),\n    };\n  }\n}\n\nclass Staff {\n  final int id;\n  final String name;\n  final String nameCN;\n  final int type;\n  final String info;\n  final int comment;\n  final bool lock;\n  final bool nsfw;\n  final Images? images;\n\n  Staff({\n    required this.id,\n    required this.name,\n    required this.nameCN,\n    required this.type,\n    required this.info,\n    required this.comment,\n    required this.lock,\n    required this.nsfw,\n    this.images,\n  });\n\n  factory Staff.fromJson(Map<String, dynamic> json) {\n    return Staff(\n      id: json['id'] is int ? json['id'] as int : 0,\n      name: json['name'] as String? ?? '',\n      nameCN: json['nameCN'] as String? ?? '',\n      type: json['type'] is int ? json['type'] as int : 0,\n      info: json['info'] as String? ?? '',\n      comment: json['comment'] is int ? json['comment'] as int : 0,\n      lock: json['lock'] as bool? ?? false,\n      nsfw: json['nsfw'] as bool? ?? false,\n      images: json['images'] != null\n          ? Images.fromJson(json['images'] as Map<String, dynamic>)\n          : null,\n    );\n  }\n\n  factory Staff.fromTemplate() {\n    return Staff(\n      id: 0,\n      name: '',\n      nameCN: '',\n      type: 0,\n      info: '',\n      comment: 0,\n      lock: false,\n      nsfw: false,\n      images: null,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    final data = <String, dynamic>{\n      'id': id,\n      'name': name,\n      'nameCN': nameCN,\n      'type': type,\n      'info': info,\n      'comment': comment,\n      'lock': lock,\n      'nsfw': nsfw,\n    };\n    if (images != null) {\n      data['images'] = images!.toJson();\n    }\n    return data;\n  }\n}\n\nclass Images {\n  final String large;\n  final String medium;\n  final String small;\n  final String grid;\n\n  Images({\n    required this.large,\n    required this.medium,\n    required this.small,\n    required this.grid,\n  });\n\n  factory Images.fromJson(Map<String, dynamic> json) {\n    return Images(\n      large: json['large'] as String? ?? '',\n      medium: json['medium'] as String? ?? '',\n      small: json['small'] as String? ?? '',\n      grid: json['grid'] as String? ?? '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'large': large,\n      'medium': medium,\n      'small': small,\n      'grid': grid,\n    };\n  }\n}\n\nclass Position {\n  final PositionType type;\n  final String summary;\n  final String appearEps;\n\n  Position({\n    required this.type,\n    required this.summary,\n    required this.appearEps,\n  });\n\n  factory Position.fromJson(Map<String, dynamic> json) {\n    return Position(\n      type: json['type'] != null\n          ? PositionType.fromJson(json['type'] as Map<String, dynamic>)\n          : PositionType.fromTemplate(),\n      summary: json['summary'] as String? ?? '',\n      appearEps: json['appearEps'] as String? ?? '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'type': type.toJson(),\n      'summary': summary,\n      'appearEps': appearEps,\n    };\n  }\n}\n\nclass PositionType {\n  final int id;\n  final String en;\n  final String cn;\n  final String jp;\n\n  PositionType({\n    required this.id,\n    required this.en,\n    required this.cn,\n    required this.jp,\n  });\n\n  factory PositionType.fromJson(Map<String, dynamic> json) {\n    return PositionType(\n      id: json['id'] is int ? json['id'] as int : 0,\n      en: json['en'] as String? ?? '',\n      cn: json['cn'] as String? ?? '',\n      jp: json['jp'] as String? ?? '',\n    );\n  }\n\n  factory PositionType.fromTemplate() {\n    return PositionType(\n      id: 0,\n      en: '',\n      cn: '',\n      jp: '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'en': en,\n      'cn': cn,\n      'jp': jp,\n    };\n  }\n}"
  },
  {
    "path": "lib/modules/staff/staff_response.dart",
    "content": "import 'package:kazumi/modules/staff/staff_item.dart';\n\nclass StaffResponse {\n  final List<StaffFullItem> data;\n  final int total;\n\n  StaffResponse({\n    required this.data,\n    required this.total,\n  });\n\n  factory StaffResponse.fromJson(Map<String, dynamic> json) {\n    return StaffResponse(\n      data: (json['data'] as List<dynamic>? ?? [])\n          .map((item) => StaffFullItem.fromJson(item as Map<String, dynamic>))\n          .toList(),\n      total: json['total'] is int ? json['total'] as int : 0,\n    );\n  }\n\n  factory StaffResponse.fromTemplate() {\n    return StaffResponse(\n      data: [],\n      total: 0,\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'data': data.map((item) => item.toJson()).toList(),\n      'total': total,\n    };\n  }\n}\n"
  },
  {
    "path": "lib/pages/about/about_module.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/pages/about/about_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/logs/logs_page.dart';\n\nclass AboutModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const AboutPage());\n    r.child(\"/logs\", child: (_) => const LogsPage());\n    r.child(\n      \"/license\",\n      child: (_) => const LicensePage(\n        applicationName: 'Kazumi',\n        applicationVersion: Api.version,\n        applicationLegalese: '开源许可证',\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/about/about_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:card_settings_ui/card_settings_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/my/my_controller.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/utils/mortis.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass AboutPage extends StatefulWidget {\n  const AboutPage({super.key});\n\n  @override\n  State<AboutPage> createState() => _AboutPageState();\n}\n\nclass _AboutPageState extends State<AboutPage> {\n  final exitBehaviorTitles = <String>['退出 Kazumi', '最小化至托盘', '每次都询问'];\n  late dynamic defaultDanmakuArea;\n  late dynamic defaultThemeMode;\n  late dynamic defaultThemeColor;\n  Box setting = GStorage.setting;\n  late int exitBehavior =\n      setting.get(SettingBoxKey.exitBehavior, defaultValue: 2);\n  late bool autoUpdate;\n  double _cacheSizeMB = -1;\n  final MyController myController = Modular.get<MyController>();\n  final MenuController menuController = MenuController();\n\n  @override\n  void initState() {\n    super.initState();\n    autoUpdate = setting.get(SettingBoxKey.autoUpdate, defaultValue: true);\n    _getCacheSize();\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  Future<Directory> _getCacheDir() async {\n    Directory tempDir = await getTemporaryDirectory();\n    return Directory('${tempDir.path}/libCachedImageData');\n  }\n\n  Future<void> _getCacheSize() async {\n    Directory cacheDir = await _getCacheDir();\n\n    if (await cacheDir.exists()) {\n      int totalSizeBytes = await _getTotalSizeOfFilesInDir(cacheDir);\n      double totalSizeMB = (totalSizeBytes / (1024 * 1024));\n\n      if (mounted) {\n        setState(() {\n          _cacheSizeMB = totalSizeMB;\n        });\n      }\n    } else {\n      if (mounted) {\n        setState(() {\n          _cacheSizeMB = 0.0;\n        });\n      }\n    }\n  }\n\n  Future<int> _getTotalSizeOfFilesInDir(final Directory directory) async {\n    final List<FileSystemEntity> children = directory.listSync();\n    int total = 0;\n\n    try {\n      for (final FileSystemEntity child in children) {\n        if (child is File) {\n          final int length = await child.length();\n          total += length;\n        } else if (child is Directory) {\n          total += await _getTotalSizeOfFilesInDir(child);\n        }\n      }\n    } catch (_) {}\n    return total;\n  }\n\n  Future<void> _clearCache() async {\n    final Directory libCacheDir = await _getCacheDir();\n    await libCacheDir.delete(recursive: true);\n    _getCacheSize();\n  }\n\n  void _showCacheDialog() {\n    KazumiDialog.show(\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('缓存管理'),\n          content: const Text('缓存为番剧封面, 清除后加载时需要重新下载,确认要清除缓存吗?'),\n          actions: [\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n              },\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () async {\n                try {\n                  _clearCache();\n                } catch (_) {}\n                KazumiDialog.dismiss();\n              },\n              child: const Text('确认'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) async {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('关于')),\n        // backgroundColor: Colors.transparent,\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/about/license');\n                  },\n                  title: Text('开源许可证', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('查看所有开源许可证', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('外部链接', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    launchUrl(Uri.parse(Api.projectUrl),\n                        mode: LaunchMode.externalApplication);\n                  },\n                  title: Text('项目主页', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    launchUrl(Uri.parse(Api.sourceUrl),\n                        mode: LaunchMode.externalApplication);\n                  },\n                  title: Text('代码仓库', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('Github', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    launchUrl(Uri.parse(Api.iconUrl),\n                        mode: LaunchMode.externalApplication);\n                  },\n                  title: Text('图标创作', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('Pixiv', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    launchUrl(Uri.parse(Api.bangumiIndex),\n                        mode: LaunchMode.externalApplication);\n                  },\n                  title: Text('番剧索引', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('Bangumi', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    launchUrl(Uri.parse(Api.dandanIndex),\n                        mode: LaunchMode.externalApplication);\n                  },\n                  title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('ID: ${mortis['id']}', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('DanDanPlay', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            if (Utils.isDesktop()) // 之后如果有非桌面平台的新选项可以移除\n              SettingsSection(\n                title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)),\n                tiles: [\n                  SettingsTile.navigation(\n                    onPressed: (_) {\n                      if (menuController.isOpen) {\n                        menuController.close();\n                      } else {\n                        menuController.open();\n                      }\n                    },\n                    title: Text('关闭时', style: TextStyle(fontFamily: fontFamily)),\n                    value: MenuAnchor(\n                      consumeOutsideTap: true,\n                      controller: menuController,\n                      builder: (_, __, ___) {\n                        return Text(exitBehaviorTitles[exitBehavior]);\n                      },\n                      menuChildren: [\n                        for (int i = 0; i < 3; i++)\n                          MenuItemButton(\n                            requestFocusOnHover: false,\n                            onPressed: () {\n                              exitBehavior = i;\n                              setting.put(SettingBoxKey.exitBehavior, i);\n                              setState(() {});\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\n                                  exitBehaviorTitles[i],\n                                  style: TextStyle(\n                                    color: i == exitBehavior\n                                        ? Theme.of(context).colorScheme.primary\n                                        : null,\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            SettingsSection(\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/about/logs');\n                  },\n                  title: Text('错误日志', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    _showCacheDialog();\n                  },\n                  title: Text('清除缓存', style: TextStyle(fontFamily: fontFamily)),\n                  value: _cacheSizeMB == -1\n                      ? Text('统计中...', style: TextStyle(fontFamily: fontFamily))\n                      : Text('${_cacheSizeMB.toStringAsFixed(2)}MB', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('应用更新', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    autoUpdate = value ?? !autoUpdate;\n                    await setting.put(SettingBoxKey.autoUpdate, autoUpdate);\n                    setState(() {});\n                  },\n                  title: Text('自动更新', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: autoUpdate,\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    myController.checkUpdate();\n                  },\n                  title: Text('检查更新', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('当前版本 ${Api.version}', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/collect/collect_controller.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:kazumi/modules/collect/collect_change_module.dart';\nimport 'package:kazumi/modules/collect/collect_type.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/webdav.dart';\nimport 'package:kazumi/repositories/collect_crud_repository.dart';\nimport 'package:kazumi/repositories/collect_repository.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:kazumi/utils/logger.dart';\n\npart 'collect_controller.g.dart';\n\nclass CollectController = _CollectController with _$CollectController;\n\nabstract class _CollectController with Store {\n  final _collectCrudRepository = Modular.get<ICollectCrudRepository>();\n  final _collectRepository = Modular.get<ICollectRepository>();\n\n  Box setting = GStorage.setting;\n  List<BangumiItem> get favorites => _collectCrudRepository.getFavorites();\n\n  @observable\n  ObservableList<CollectedBangumi> collectibles =\n      ObservableList<CollectedBangumi>();\n\n  void loadCollectibles() {\n    collectibles.clear();\n    collectibles.addAll(_collectCrudRepository.getAllCollectibles());\n  }\n\n  int getCollectType(BangumiItem bangumiItem) {\n    return _collectCrudRepository.getCollectType(bangumiItem.id);\n  }\n\n  @action\n  Future<void> addCollect(BangumiItem bangumiItem, {type = 1}) async {\n    if (type == 0) {\n      await deleteCollect(bangumiItem);\n      return;\n    }\n    await _collectCrudRepository.addCollectible(bangumiItem, type);\n    final int collectChangeId = (DateTime.now().millisecondsSinceEpoch ~/ 1000);\n    final CollectedBangumiChange collectChange = CollectedBangumiChange(\n        collectChangeId,\n        bangumiItem.id,\n        1,\n        type,\n        (DateTime.now().millisecondsSinceEpoch ~/ 1000));\n    await _collectCrudRepository.addCollectChange(collectChange);\n    loadCollectibles();\n  }\n\n  @action\n  Future<void> deleteCollect(BangumiItem bangumiItem) async {\n    await _collectCrudRepository.deleteCollectible(bangumiItem.id);\n    final int collectChangeId = (DateTime.now().millisecondsSinceEpoch ~/ 1000);\n    final CollectedBangumiChange collectChange = CollectedBangumiChange(\n        collectChangeId,\n        bangumiItem.id,\n        3,\n        5,\n        (DateTime.now().millisecondsSinceEpoch ~/ 1000));\n    await _collectCrudRepository.addCollectChange(collectChange);\n    loadCollectibles();\n  }\n\n  Future<void> updateLocalCollect(BangumiItem bangumiItem) async {\n    await _collectCrudRepository.updateCollectible(bangumiItem);\n    loadCollectibles();\n  }\n\n  Future<void> syncCollectibles() async {\n    if (!WebDav().initialized) {\n      KazumiDialog.showToast(message: '未开启WebDav同步或配置无效');\n      return;\n    }\n    bool flag = true;\n    try {\n      await WebDav().ping();\n    } catch (e) {\n      KazumiLogger().e('WebDav: WebDav connection failed', error: e);\n      KazumiDialog.showToast(message: 'WebDav连接失败: $e');\n      flag = false;\n    }\n    if (!flag) {\n      return;\n    }\n    try {\n      await WebDav().syncCollectibles();\n    } catch (e){\n      KazumiDialog.showToast(message: 'WebDav同步失败 $e');\n    }\n    loadCollectibles();\n  }\n\n  // migrate collect from old version (favorites)\n  Future<void> migrateCollect() async {\n    if (favorites.isNotEmpty) {\n      int count = 0;\n      for (BangumiItem bangumiItem in favorites) {\n        await addCollect(bangumiItem, type: 1);\n        count++;\n      }\n      await _collectCrudRepository.clearFavorites();\n      KazumiLogger().d('GStorage: detected $count uncategorized favorites, migrated to collectibles');\n    }\n  }\n\n  /// 根据收藏类型获取番剧ID集合\n  ///\n  /// [type] 收藏类型\n  /// 返回番剧ID集合\n  Set<int> getBangumiIdsByType(CollectType type) {\n    return _collectRepository.getBangumiIdsByType(type);\n  }\n\n  /// 过滤掉指定收藏类型的番剧\n  ///\n  /// [bangumiList] 原始番剧列表\n  /// [excludeType] 要排除的收藏类型\n  /// 返回过滤后的番剧列表\n  List<BangumiItem> filterBangumiByType(\n      List<BangumiItem> bangumiList, CollectType excludeType) {\n    final excludeIds = getBangumiIdsByType(excludeType);\n    return bangumiList\n        .where((item) => !excludeIds.contains(item.id))\n        .toList();\n  }\n}\n"
  },
  {
    "path": "lib/pages/collect/collect_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'collect_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$CollectController on _CollectController, Store {\n  late final _$collectiblesAtom =\n      Atom(name: '_CollectController.collectibles', context: context);\n\n  @override\n  ObservableList<CollectedBangumi> get collectibles {\n    _$collectiblesAtom.reportRead();\n    return super.collectibles;\n  }\n\n  @override\n  set collectibles(ObservableList<CollectedBangumi> value) {\n    _$collectiblesAtom.reportWrite(value, super.collectibles, () {\n      super.collectibles = value;\n    });\n  }\n\n  late final _$addCollectAsyncAction =\n      AsyncAction('_CollectController.addCollect', context: context);\n\n  @override\n  Future<void> addCollect(BangumiItem bangumiItem, {dynamic type = 1}) {\n    return _$addCollectAsyncAction\n        .run(() => super.addCollect(bangumiItem, type: type));\n  }\n\n  late final _$deleteCollectAsyncAction =\n      AsyncAction('_CollectController.deleteCollect', context: context);\n\n  @override\n  Future<void> deleteCollect(BangumiItem bangumiItem) {\n    return _$deleteCollectAsyncAction\n        .run(() => super.deleteCollect(bangumiItem));\n  }\n\n  @override\n  String toString() {\n    return '''\ncollectibles: ${collectibles}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/collect/collect_module.dart",
    "content": "import 'package:kazumi/pages/collect/collect_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass CollectModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const CollectPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/collect/collect_page.dart",
    "content": "import 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/pages/menu/menu.dart';\nimport 'package:kazumi/bean/card/bangumi_card.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:provider/provider.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\n\nclass CollectPage extends StatefulWidget {\n  const CollectPage({super.key});\n\n  @override\n  State<CollectPage> createState() => _CollectPageState();\n}\n\nclass _CollectPageState extends State<CollectPage>\n    with SingleTickerProviderStateMixin {\n  final CollectController collectController = Modular.get<CollectController>();\n  late NavigationBarState navigationBarState;\n  TabController? tabController;\n  bool showDelete = false;\n  bool syncCollectiblesing = false;\n  Box setting = GStorage.setting;\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n    navigationBarState.updateSelectedIndex(0);\n    Modular.to.navigate('/tab/popular/');\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    collectController.loadCollectibles();\n    tabController = TabController(vsync: this, length: tabs.length);\n    navigationBarState =\n        Provider.of<NavigationBarState>(context, listen: false);\n  }\n\n  @override\n  void dispose() {\n    tabController?.dispose();\n    super.dispose();\n  }\n\n  final List<Tab> tabs = const <Tab>[\n    Tab(text: '在看'),\n    Tab(text: '想看'),\n    Tab(text: '搁置'),\n    Tab(text: '看过'),\n    Tab(text: '抛弃'),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: SysAppBar(\n          needTopOffset: false,\n          toolbarHeight: 104,\n          bottom: TabBar(\n            controller: tabController,\n            tabs: tabs,\n            indicatorColor: Theme.of(context).colorScheme.primary,\n          ),\n          title: const Text('追番'),\n          actions: [\n            IconButton(\n                onPressed: () {\n                  setState(() {\n                    showDelete = !showDelete;\n                  });\n                },\n                icon: showDelete\n                    ? const Icon(Icons.edit_outlined)\n                    : const Icon(Icons.edit))\n          ],\n        ),\n        floatingActionButton: FloatingActionButton(\n          onPressed: () async {\n            bool webDavenable = await setting.get(SettingBoxKey.webDavEnable,\n                defaultValue: false);\n            if (!webDavenable) {\n              KazumiDialog.showToast(message: 'webDav未启用, 同步功能不可用');\n              return;\n            }\n            if (showDelete) {\n              KazumiDialog.showToast(message: '编辑模式无法执行同步');\n              return;\n            }\n            if (syncCollectiblesing) {\n              return;\n            }\n            setState(() {\n              syncCollectiblesing = true;\n            });\n            await collectController.syncCollectibles();\n            setState(() {\n              syncCollectiblesing = false;\n            });\n          },\n          child: syncCollectiblesing\n              ? const SizedBox(\n                  width: 32, height: 32, child: CircularProgressIndicator())\n              : const Icon(Icons.cloud_sync),\n        ),\n        body: Observer(builder: (context) {\n          return renderBody;\n        }),\n      ),\n    );\n  }\n\n  Widget get renderBody {\n    if (collectController.collectibles.isNotEmpty) {\n      return TabBarView(\n        controller: tabController,\n        children: contentGrid(collectController.collectibles),\n      );\n    } else {\n      return const Center(\n        child: Text('啊嘞, 没有追番的说 (´;ω;`)'),\n      );\n    }\n  }\n\n  List<Widget> contentGrid(List<CollectedBangumi> collectedBangumiList) {\n    List<Widget> gridViewList = [];\n    List<List<CollectedBangumi>> collectedBangumiRenderItemList =\n        List.generate(tabs.length, (_) => <CollectedBangumi>[]);\n    for (CollectedBangumi element in collectedBangumiList) {\n      collectedBangumiRenderItemList[element.type - 1].add(element);\n    }\n    for (List<CollectedBangumi> list in collectedBangumiRenderItemList) {\n      list.sort((a, b) => b.time.millisecondsSinceEpoch\n          .compareTo(a.time.millisecondsSinceEpoch));\n    }\n    int crossCount = 3;\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {\n      crossCount = 5;\n    }\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {\n      crossCount = 6;\n    }\n    for (List<CollectedBangumi> collectedBangumiRenderItem\n        in collectedBangumiRenderItemList) {\n      gridViewList.add(\n        CustomScrollView(\n          slivers: [\n            SliverPadding(\n              padding: const EdgeInsets.fromLTRB(\n                  StyleString.cardSpace, StyleString.cardSpace, StyleString.cardSpace, 0),\n              sliver: SliverGrid(\n                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n                  mainAxisSpacing: StyleString.cardSpace - 2,\n                  crossAxisSpacing: StyleString.cardSpace,\n                  crossAxisCount: crossCount,\n                  mainAxisExtent:\n                      MediaQuery.of(context).size.width / crossCount / 0.65 +\n                          MediaQuery.textScalerOf(context).scale(32.0),\n                ),\n                delegate: SliverChildBuilderDelegate(\n                  (BuildContext context, int index) {\n                    return collectedBangumiRenderItem.isNotEmpty\n                        ? Stack(\n                            children: [\n                              BangumiCardV(\n                                bangumiItem: collectedBangumiRenderItem[index]\n                                    .bangumiItem,\n                                canTap: !showDelete,\n                              ),\n                              Positioned(\n                                right: 5,\n                                bottom: 5,\n                                child: showDelete\n                                    ? Container(\n                                        width: 40,\n                                        height: 40,\n                                        decoration: BoxDecoration(\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .secondaryContainer,\n                                          shape: BoxShape.circle,\n                                        ),\n                                        child: CollectButton(\n                                          bangumiItem:\n                                              collectedBangumiRenderItem[index]\n                                                  .bangumiItem,\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .onSecondaryContainer,\n                                        ),\n                                      )\n                                    : Container(),\n                              ),\n                            ],\n                          )\n                        : null;\n                  },\n                  childCount: collectedBangumiRenderItem.isNotEmpty\n                      ? collectedBangumiRenderItem.length\n                      : 10,\n                ),\n              ),\n            ),\n          ],\n        ),\n      );\n    }\n    return gridViewList;\n  }\n}\n"
  },
  {
    "path": "lib/pages/download/download_controller.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_module.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/repositories/download_repository.dart';\nimport 'package:kazumi/utils/background_download_service.dart';\nimport 'package:kazumi/utils/download_manager.dart';\nimport 'package:kazumi/utils/format_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/providers/video/providers.dart';\nimport 'package:kazumi/request/damaku.dart';\nimport 'package:mobx/mobx.dart';\n\npart 'download_controller.g.dart';\n\nclass DownloadController = _DownloadController with _$DownloadController;\n\nabstract class _DownloadController with Store {\n  final _repository = Modular.get<IDownloadRepository>();\n  final _downloadManager = Modular.get<IDownloadManager>();\n  final _backgroundService = BackgroundDownloadService();\n\n  @observable\n  ObservableList<DownloadRecord> records = ObservableList<DownloadRecord>();\n\n  final List<_ResolveRequest> _resolveQueue = [];\n  bool _isResolving = false;\n  bool _isBackgroundServiceInitialized = false;\n\n  Future<void> init() async {\n    final temp = _repository.getAllRecords();\n    records.clear();\n    records.addAll(temp);\n\n    // Reset any incomplete states to 'paused' on startup\n    // This includes 'pending' because the in-memory queue is lost on restart\n    for (final record in records) {\n      bool changed = false;\n      for (final entry in record.episodes.entries) {\n        if (entry.value.status == DownloadStatus.downloading ||\n            entry.value.status == DownloadStatus.resolving ||\n            entry.value.status == DownloadStatus.pending) {\n          entry.value.status = DownloadStatus.paused;\n          changed = true;\n        }\n      }\n      if (changed) {\n        _repository.putRecord(record);\n      }\n    }\n\n    // 将旧 Hive danmakuData 迁移到独立文件，防止 Hive compact 时 OOM\n    await _migrateDanmakuDataToFiles();\n\n    _downloadManager.onProgress = _onDownloadProgress;\n    await _initBackgroundService();\n  }\n\n  /// 启动时将所有旧 Hive danmakuData 迁移到独立文件并清空 Hive 字段\n  Future<void> _migrateDanmakuDataToFiles() async {\n    int migratedCount = 0;\n    for (final record in records) {\n      bool recordChanged = false;\n      for (final entry in record.episodes.entries) {\n        final episode = entry.value;\n        if (episode.danmakuData.isEmpty ||\n            episode.downloadDirectory.isEmpty) {\n          continue;\n        }\n        try {\n          // danmakuData 已经是弹幕数组的 JSON 字符串，直接拼接成新格式写入\n          // 避免 jsonDecode → Danmaku.fromJson × N → toJson × N → jsonEncode 的开销\n          final file = File(_danmakuFilePath(episode.downloadDirectory));\n          await file.writeAsString(\n              '{\"danDanBangumiID\":${episode.danDanBangumiID},\"danmakus\":${episode.danmakuData}}');\n          episode.danmakuData = '';\n          recordChanged = true;\n          migratedCount++;\n        } catch (e) {\n          KazumiLogger().w(\n              'DownloadController: danmaku migration failed for episode ${entry.key}',\n              error: e);\n        }\n      }\n      if (recordChanged) {\n        await _repository.putRecord(record);\n      }\n    }\n    if (migratedCount > 0) {\n      KazumiLogger().i(\n          'DownloadController: migrated danmaku data for $migratedCount episodes');\n    }\n  }\n\n  Future<void> _initBackgroundService() async {\n    if (!_backgroundService.isSupported) return;\n    if (_isBackgroundServiceInitialized) return;\n\n    await _backgroundService.init();\n    _backgroundService.onPauseAll = pauseAllDownloads;\n    _backgroundService.addTaskDataCallback(_onTaskData);\n    _isBackgroundServiceInitialized = true;\n  }\n\n  void _onTaskData(Object data) {\n    if (data is Map) {\n      final action = data['action'];\n      if (action == 'button_pressed') {\n        _backgroundService.handleNotificationAction(data['id'] as String);\n      } else if (action == 'navigate_to_download') {\n        _backgroundService.handleNavigateToDownload();\n      }\n    }\n  }\n\n  final Map<String, double> _speeds = {};\n  DateTime _lastUiUpdateTime = DateTime.now();\n  static const _uiUpdateInterval = Duration(milliseconds: 500);\n\n  void _onDownloadProgress(String recordKey, int episodeNumber,\n      DownloadEpisode episode, double speed) {\n    final record = _repository.getRecord(recordKey);\n    if (record == null || !record.episodes.containsKey(episodeNumber)) {\n      return;\n    }\n    _repository.updateEpisode(recordKey, episodeNumber, episode);\n\n    final key = '${recordKey}_$episodeNumber';\n    _speeds[key] = speed;\n\n    final isFinalState = episode.status == DownloadStatus.completed ||\n        episode.status == DownloadStatus.failed ||\n        episode.status == DownloadStatus.paused;\n\n    final now = DateTime.now();\n    if (isFinalState || now.difference(_lastUiUpdateTime) >= _uiUpdateInterval) {\n      _lastUiUpdateTime = now;\n      refreshRecords();\n      _updateBackgroundNotification();\n    }\n  }\n\n  Future<void> _updateBackgroundNotification() async {\n    if (!_backgroundService.isRunning) return;\n\n    final stats = _getDownloadStats();\n    if (stats.activeCount == 0 && stats.pendingCount == 0) {\n      await _backgroundService.stopService();\n      return;\n    }\n\n    double totalSpeed = 0;\n    for (final entry in _speeds.entries) {\n      totalSpeed += entry.value;\n    }\n\n    await _backgroundService.updateProgress(\n      activeCount: stats.activeCount,\n      totalCount: stats.totalCount,\n      overallProgress: stats.overallProgress,\n      speedText: formatSpeed(totalSpeed),\n    );\n  }\n\n  Future<void> _startBackgroundServiceIfNeeded() async {\n    if (!_backgroundService.isSupported || _backgroundService.isRunning) return;\n\n    final started = await _backgroundService.startService();\n    if (started) {\n      KazumiLogger().i('DownloadController: background service started');\n    }\n  }\n\n  ({int activeCount, int pendingCount, int totalCount, double overallProgress}) _getDownloadStats() {\n    int activeCount = 0;\n    int pendingCount = 0;\n    int totalCount = 0;\n    double totalProgress = 0;\n\n    for (final record in records) {\n      for (final episode in record.episodes.values) {\n        if (episode.status == DownloadStatus.downloading) {\n          activeCount++;\n          totalCount++;\n          totalProgress += episode.progressPercent;\n        } else if (episode.status == DownloadStatus.resolving ||\n            episode.status == DownloadStatus.pending) {\n          pendingCount++;\n          totalCount++;\n        }\n      }\n    }\n\n    final overallProgress = totalCount > 0 ? totalProgress / totalCount : 0.0;\n    return (\n      activeCount: activeCount,\n      pendingCount: pendingCount,\n      totalCount: totalCount,\n      overallProgress: overallProgress,\n    );\n  }\n\n  double getSpeed(int bangumiId, String pluginName, int episodeNumber) {\n    final key = '${pluginName}_${bangumiId}_$episodeNumber';\n    return _speeds[key] ?? 0.0;\n  }\n\n\n\n  @action\n  void refreshRecords() {\n    final temp = _repository.getAllRecords();\n    records.clear();\n    records.addAll(temp);\n  }\n\n  Plugin? _findPlugin(String pluginName) {\n    final pluginsController = Modular.get<PluginsController>();\n    for (final plugin in pluginsController.pluginList) {\n      if (plugin.name == pluginName) return plugin;\n    }\n    return null;\n  }\n\n  DownloadRecord? getRecord(int bangumiId, String pluginName) {\n    return _repository.getRecordByBangumiId(bangumiId, pluginName);\n  }\n\n  DownloadEpisode? getEpisode(\n      int bangumiId, String pluginName, int episodeNumber) {\n    return _repository.getEpisode(bangumiId, pluginName, episodeNumber);\n  }\n\n  DownloadEpisode? getEpisodeByUrl(\n      int bangumiId, String pluginName, String episodePageUrl) {\n    return _repository.getEpisodeByUrl(bangumiId, pluginName, episodePageUrl);\n  }\n\n  String? getLocalVideoPath(\n      int bangumiId, String pluginName, int episodeNumber) {\n    final episode =\n        _repository.getEpisode(bangumiId, pluginName, episodeNumber);\n    return _downloadManager.getLocalVideoPath(episode);\n  }\n\n  List<DownloadEpisode> getCompletedEpisodes(int bangumiId, String pluginName) {\n    return _repository.getCompletedEpisodes(bangumiId, pluginName);\n  }\n\n  /// 弹幕文件路径\n  String _danmakuFilePath(String downloadDirectory) {\n    return '$downloadDirectory/danmaku.json';\n  }\n\n  /// 从文件读取弹幕数据\n  /// 支持新格式 (带 danDanBangumiID 的 wrapper) 和旧格式 (纯数组)\n  Future<({List<Danmaku> danmakus, int danDanBangumiID})?> _readDanmakuFromFile(\n      String downloadDirectory) async {\n    if (downloadDirectory.isEmpty) return null;\n    final file = File(_danmakuFilePath(downloadDirectory));\n    if (!await file.exists()) return null;\n    try {\n      final content = await file.readAsString();\n      final decoded = jsonDecode(content);\n      if (decoded is List) {\n        // 旧格式：纯弹幕数组\n        final danmakus =\n            decoded.map((json) => Danmaku.fromJson(json)).toList();\n        return (danmakus: danmakus, danDanBangumiID: 0);\n      } else if (decoded is Map<String, dynamic>) {\n        // 新格式：带 danDanBangumiID 的 wrapper\n        final danDanBangumiID = decoded['danDanBangumiID'] as int? ?? 0;\n        final List<dynamic> jsonList = decoded['danmakus'] as List? ?? [];\n        final danmakus =\n            jsonList.map((json) => Danmaku.fromJson(json)).toList();\n        return (danmakus: danmakus, danDanBangumiID: danDanBangumiID);\n      }\n      return null;\n    } catch (e) {\n      KazumiLogger()\n          .w('DownloadController: failed to read danmaku file', error: e);\n      return null;\n    }\n  }\n\n  /// 写入弹幕数据到文件 (新格式，包含 danDanBangumiID)\n  Future<void> _writeDanmakuToFile(\n      String downloadDirectory, List<Danmaku> danmakus, int danDanBangumiID) async {\n    if (downloadDirectory.isEmpty) return;\n    final file = File(_danmakuFilePath(downloadDirectory));\n    final wrapper = {\n      'danDanBangumiID': danDanBangumiID,\n      'danmakus': danmakus.map((d) => d.toJson()).toList(),\n    };\n    await file.writeAsString(jsonEncode(wrapper));\n  }\n\n  Future<List<Danmaku>?> getCachedDanmakus(\n      int bangumiId, String pluginName, int episodeNumber) async {\n    final episode =\n        _repository.getEpisode(bangumiId, pluginName, episodeNumber);\n    if (episode == null) return null;\n\n    // 从文件读取\n    final fromFile = await _readDanmakuFromFile(episode.downloadDirectory);\n    if (fromFile != null && fromFile.danmakus.isNotEmpty) {\n      return fromFile.danmakus;\n    }\n\n    return null;\n  }\n\n  Future<void> updateCachedDanmakus(\n    int bangumiId,\n    String pluginName,\n    int episodeNumber,\n    List<Danmaku> danmakus,\n    int danDanBangumiID,\n  ) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    final record = _repository.getRecord(recordKey);\n    if (record == null) return;\n    final episode = record.episodes[episodeNumber];\n    if (episode == null) return;\n\n    try {\n      // 写入独立文件而非 Hive\n      await _writeDanmakuToFile(\n          episode.downloadDirectory, danmakus, danDanBangumiID);\n      // 确保 Hive 中不存储弹幕大数据\n      if (episode.danmakuData.isNotEmpty) {\n        episode.danmakuData = '';\n        await _repository.updateEpisode(recordKey, episodeNumber, episode);\n      }\n      KazumiLogger().i(\n          'DownloadController: updated cached danmakus for episode $episodeNumber');\n    } catch (e) {\n      KazumiLogger()\n          .w('DownloadController: failed to update cached danmaku', error: e);\n    }\n  }\n\n  Future<void> startDownload({\n    required int bangumiId,\n    required String bangumiName,\n    required String bangumiCover,\n    required String pluginName,\n    required int episodeNumber,\n    required String episodeName,\n    required int road,\n    required String episodePageUrl,\n  }) async {\n    final recordKey = '${pluginName}_$bangumiId';\n\n    final record = _repository.getRecord(recordKey) ??\n        DownloadRecord(\n          bangumiId,\n          bangumiName,\n          bangumiCover,\n          pluginName,\n          {},\n          DateTime.now(),\n        );\n\n    if (episodePageUrl.isNotEmpty) {\n      for (final entry in record.episodes.entries) {\n        if (entry.value.episodePageUrl == episodePageUrl) {\n          KazumiLogger().i(\n              'DownloadController: episode URL already exists at position ${entry.key}, skipping');\n          return;\n        }\n      }\n    }\n\n    final episode = DownloadEpisode(\n      episodeNumber,\n      episodeName,\n      road,\n      DownloadStatus.resolving,\n      0.0,\n      0,\n      0,\n      '',\n      '',\n      '',\n      null,\n      '',\n      0,\n      episodePageUrl,\n    );\n\n    record.episodes[episodeNumber] = episode;\n    await _repository.putRecord(record);\n    refreshRecords();\n\n    _resolveQueue.add(_ResolveRequest(\n      recordKey: recordKey,\n      bangumiId: bangumiId,\n      pluginName: pluginName,\n      episodeNumber: episodeNumber,\n      episodePageUrl: episodePageUrl,\n    ));\n    _processResolveQueue();\n  }\n\n  Future<void> _processResolveQueue() async {\n    if (_isResolving || _resolveQueue.isEmpty) return;\n    _isResolving = true;\n\n    while (_resolveQueue.isNotEmpty) {\n      final request = _resolveQueue.removeAt(0);\n      await _resolveAndEnqueue(request);\n    }\n\n    _isResolving = false;\n  }\n\n  Future<void> _resolveAndEnqueue(_ResolveRequest request) async {\n    final plugin = _findPlugin(request.pluginName);\n    if (plugin == null) {\n      _failEpisode(request.recordKey, request.episodeNumber,\n          '找不到插件 ${request.pluginName}');\n      return;\n    }\n\n    final record = _repository.getRecord(request.recordKey);\n    if (record == null) return;\n    final episode = record.episodes[request.episodeNumber];\n    if (episode == null) return;\n\n    if (episode.status != DownloadStatus.resolving) return;\n\n    final fullUrl = plugin.buildFullUrl(request.episodePageUrl);\n\n    KazumiLogger().i(\n        'DownloadController: resolving video URL for episode ${request.episodeNumber} from $fullUrl');\n\n    String? m3u8Url;\n    final provider = WebViewVideoSourceProvider();\n    try {\n      final source = await provider.resolve(\n        fullUrl,\n        useLegacyParser: plugin.useLegacyParser,\n        timeout: const Duration(seconds: 30),\n      );\n      m3u8Url = source.url;\n    } on VideoSourceTimeoutException {\n      KazumiLogger().w('DownloadController: WebView resolution timed out');\n    } on VideoSourceCancelledException {\n      KazumiLogger().i('DownloadController: WebView resolution cancelled');\n    } catch (e) {\n      KazumiLogger()\n          .e('DownloadController: WebView resolution failed', error: e);\n    } finally {\n      provider.dispose();\n    }\n\n    if (m3u8Url == null || m3u8Url.isEmpty) {\n      _failEpisode(request.recordKey, request.episodeNumber, '解析视频源超时');\n      return;\n    }\n\n    KazumiLogger().i(\n        'DownloadController: resolved M3U8 URL for episode ${request.episodeNumber}: $m3u8Url');\n\n    // Update episode with resolved URL\n    final freshRecord = _repository.getRecord(request.recordKey);\n    if (freshRecord == null) return;\n    final freshEpisode = freshRecord.episodes[request.episodeNumber];\n    if (freshEpisode == null) return;\n\n    if (freshEpisode.status != DownloadStatus.resolving) return;\n\n    freshEpisode.networkM3u8Url = m3u8Url;\n    freshEpisode.status = DownloadStatus.downloading;\n    await _repository.updateEpisode(\n        request.recordKey, request.episodeNumber, freshEpisode);\n    refreshRecords();\n\n    await _startBackgroundServiceIfNeeded();\n\n    final httpHeaders = plugin.buildHttpHeaders();\n    bool adBlockerEnabled = _repository.getForceAdBlocker() || plugin.adBlocker;\n\n    await _downloadManager.enqueue(DownloadRequest(\n      recordKey: request.recordKey,\n      bangumiId: request.bangumiId,\n      pluginName: request.pluginName,\n      episodeNumber: request.episodeNumber,\n      m3u8Url: m3u8Url,\n      httpHeaders: httpHeaders,\n      adBlockerEnabled: adBlockerEnabled,\n      episode: freshEpisode,\n    ));\n\n    final Box setting = GStorage.setting;\n    final bool downloadDanmaku =\n        setting.get(SettingBoxKey.downloadDanmaku, defaultValue: true);\n    if (downloadDanmaku) {\n      _fetchAndCacheDanmakuAsync(\n        request.recordKey,\n        request.bangumiId,\n        request.episodeNumber,\n      );\n    }\n  }\n\n  void _fetchAndCacheDanmakuAsync(\n      String recordKey, int bangumiId, int episodeNumber) {\n    Future(() async {\n      try {\n        KazumiLogger().i(\n            'DownloadController: fetching danmaku for episode $episodeNumber (async)');\n\n        // 获取 DanDan 番剧 ID\n        final danDanBangumiID =\n            await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bangumiId);\n        if (danDanBangumiID == 0) {\n          KazumiLogger().w(\n              'DownloadController: failed to get DanDan bangumiID for $bangumiId');\n          return;\n        }\n\n        // 获取弹幕列表\n        final danmakus =\n            await DanmakuRequest.getDanDanmaku(danDanBangumiID, episodeNumber);\n        if (danmakus.isEmpty) {\n          KazumiLogger().i(\n              'DownloadController: no danmaku found for episode $episodeNumber');\n          return;\n        }\n\n        // 等待 downloadDirectory 就绪（下载管理器处理任务后才设置）\n        String downloadDirectory = '';\n        for (int i = 0; i < 10; i++) {\n          final record = _repository.getRecord(recordKey);\n          final episode = record?.episodes[episodeNumber];\n          if (episode == null) return;\n          if (episode.status == DownloadStatus.failed ||\n              episode.status == DownloadStatus.paused) {\n            return;\n          }\n          if (episode.downloadDirectory.isNotEmpty) {\n            downloadDirectory = episode.downloadDirectory;\n            break;\n          }\n          await Future.delayed(const Duration(seconds: 3));\n        }\n        if (downloadDirectory.isEmpty) {\n          KazumiLogger().w(\n              'DownloadController: downloadDirectory not ready for episode $episodeNumber, skipping danmaku cache');\n          return;\n        }\n\n        // 写入独立文件\n        await _writeDanmakuToFile(\n            downloadDirectory, danmakus, danDanBangumiID);\n\n        KazumiLogger().i(\n            'DownloadController: cached ${danmakus.length} danmakus for episode $episodeNumber');\n      } catch (e) {\n        // 弹幕获取失败不影响下载\n        KazumiLogger()\n            .w('DownloadController: failed to fetch danmaku', error: e);\n      }\n    });\n  }\n\n  void _failEpisode(String recordKey, int episodeNumber, String message) {\n    final record = _repository.getRecord(recordKey);\n    if (record == null) return;\n    final episode = record.episodes[episodeNumber];\n    if (episode == null) return;\n    episode.status = DownloadStatus.failed;\n    episode.errorMessage = message;\n    _repository.updateEpisode(recordKey, episodeNumber, episode);\n    refreshRecords();\n    KazumiLogger()\n        .w('DownloadController: episode $episodeNumber failed: $message');\n  }\n\n  Future<void> pauseDownload(\n      int bangumiId, String pluginName, int episodeNumber) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    _downloadManager.pause(recordKey, episodeNumber);\n\n    _resolveQueue.removeWhere(\n        (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber);\n\n    final record = _repository.getRecord(recordKey);\n    if (record != null) {\n      final episode = record.episodes[episodeNumber];\n      if (episode != null) {\n        episode.status = DownloadStatus.paused;\n        await _repository.updateEpisode(recordKey, episodeNumber, episode);\n        refreshRecords();\n        _updateBackgroundNotification();\n      }\n    }\n  }\n\n  Future<void> pauseAllDownloads() async {\n    KazumiLogger().i('DownloadController: pausing all downloads');\n\n    _resolveQueue.clear();\n\n    for (final record in records) {\n      for (final entry in record.episodes.entries) {\n        final episode = entry.value;\n        if (episode.status == DownloadStatus.downloading ||\n            episode.status == DownloadStatus.resolving ||\n            episode.status == DownloadStatus.pending) {\n          final recordKey = '${record.pluginName}_${record.bangumiId}';\n          _downloadManager.pause(recordKey, entry.key);\n          episode.status = DownloadStatus.paused;\n          await _repository.updateEpisode(recordKey, entry.key, episode);\n        }\n      }\n    }\n\n    refreshRecords();\n\n    await _backgroundService.stopService();\n  }\n\n  Future<void> retryDownload({\n    required int bangumiId,\n    required String pluginName,\n    required int episodeNumber,\n  }) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    final record = _repository.getRecord(recordKey);\n    if (record == null) return;\n    final episode = record.episodes[episodeNumber];\n    if (episode == null) return;\n\n    final plugin = _findPlugin(pluginName);\n    if (plugin == null) {\n      _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName');\n      return;\n    }\n\n    // If we already have a resolved M3U8 URL, go directly to download\n    if (episode.networkM3u8Url.isNotEmpty) {\n      episode.status = DownloadStatus.downloading;\n      episode.errorMessage = '';\n      episode.progressPercent = 0.0;\n      episode.downloadedSegments = 0;\n      await _repository.updateEpisode(recordKey, episodeNumber, episode);\n      refreshRecords();\n\n      await _startBackgroundServiceIfNeeded();\n\n      final httpHeaders = plugin.buildHttpHeaders();\n      bool adBlockerEnabled =\n          _repository.getForceAdBlocker() || plugin.adBlocker;\n\n      await _downloadManager.enqueue(DownloadRequest(\n        recordKey: recordKey,\n        bangumiId: bangumiId,\n        pluginName: pluginName,\n        episodeNumber: episodeNumber,\n        m3u8Url: episode.networkM3u8Url,\n        httpHeaders: httpHeaders,\n        adBlockerEnabled: adBlockerEnabled,\n        episode: episode,\n      ));\n    } else {\n      episode.status = DownloadStatus.resolving;\n      episode.errorMessage = '';\n      episode.progressPercent = 0.0;\n      episode.downloadedSegments = 0;\n      await _repository.updateEpisode(recordKey, episodeNumber, episode);\n      refreshRecords();\n\n      _resolveQueue.add(_ResolveRequest(\n        recordKey: recordKey,\n        bangumiId: bangumiId,\n        pluginName: pluginName,\n        episodeNumber: episodeNumber,\n        episodePageUrl: episode.episodePageUrl,\n      ));\n      _processResolveQueue();\n    }\n  }\n\n  Future<void> cancelDownload(\n      int bangumiId, String pluginName, int episodeNumber) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    _downloadManager.cancel(recordKey, episodeNumber);\n    _resolveQueue.removeWhere(\n        (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber);\n    await _downloadManager.deleteEpisodeFiles(\n        bangumiId, pluginName, episodeNumber);\n    await _repository.deleteEpisode(recordKey, episodeNumber);\n    refreshRecords();\n    _updateBackgroundNotification();\n  }\n\n  Future<void> deleteRecord(int bangumiId, String pluginName) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    final record = _repository.getRecord(recordKey);\n    if (record != null) {\n      for (final ep in record.episodes.keys) {\n        _downloadManager.cancel(recordKey, ep);\n        _speeds.remove('${recordKey}_$ep');\n      }\n    }\n    _resolveQueue.removeWhere((r) => r.recordKey == recordKey);\n    await _downloadManager.deleteRecordFiles(bangumiId, pluginName);\n    await _repository.deleteRecord(recordKey);\n    refreshRecords();\n    _updateBackgroundNotification();\n  }\n\n  Future<void> deleteEpisode(\n      int bangumiId, String pluginName, int episodeNumber) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    _downloadManager.cancel(recordKey, episodeNumber);\n    _speeds.remove('${recordKey}_$episodeNumber');\n    _resolveQueue.removeWhere(\n        (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber);\n    await _downloadManager.deleteEpisodeFiles(\n        bangumiId, pluginName, episodeNumber);\n    await _repository.deleteEpisode(recordKey, episodeNumber);\n    refreshRecords();\n    _updateBackgroundNotification();\n  }\n\n  Future<void> priorityDownload({\n    required int bangumiId,\n    required String pluginName,\n    required int episodeNumber,\n  }) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    final record = _repository.getRecord(recordKey);\n    if (record == null) return;\n    final episode = record.episodes[episodeNumber];\n    if (episode == null) return;\n\n    final plugin = _findPlugin(pluginName);\n    if (plugin == null) {\n      _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName');\n      return;\n    }\n\n    _resolveQueue.removeWhere(\n        (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber);\n\n    if (episode.networkM3u8Url.isNotEmpty) {\n      episode.status = DownloadStatus.downloading;\n      episode.errorMessage = '';\n      await _repository.updateEpisode(recordKey, episodeNumber, episode);\n      refreshRecords();\n\n      await _startBackgroundServiceIfNeeded();\n\n      final httpHeaders = plugin.buildHttpHeaders();\n      bool adBlockerEnabled =\n          _repository.getForceAdBlocker() || plugin.adBlocker;\n\n      await _downloadManager.enqueuePriority(DownloadRequest(\n        recordKey: recordKey,\n        bangumiId: bangumiId,\n        pluginName: pluginName,\n        episodeNumber: episodeNumber,\n        m3u8Url: episode.networkM3u8Url,\n        httpHeaders: httpHeaders,\n        adBlockerEnabled: adBlockerEnabled,\n        episode: episode,\n      ));\n    } else {\n      episode.status = DownloadStatus.resolving;\n      episode.errorMessage = '';\n      await _repository.updateEpisode(recordKey, episodeNumber, episode);\n      refreshRecords();\n\n      _resolveQueue.insert(\n          0,\n          _ResolveRequest(\n            recordKey: recordKey,\n            bangumiId: bangumiId,\n            pluginName: pluginName,\n            episodeNumber: episodeNumber,\n            episodePageUrl: episode.episodePageUrl,\n          ));\n      _processResolveQueue();\n    }\n  }\n\n  Future<void> resumeAllDownloads(int bangumiId, String pluginName) async {\n    final recordKey = '${pluginName}_$bangumiId';\n    final record = _repository.getRecord(recordKey);\n    if (record == null) return;\n\n    final incompleteEpisodes = record.episodes.entries\n        .where((e) =>\n            e.value.status == DownloadStatus.paused ||\n            e.value.status == DownloadStatus.failed ||\n            e.value.status == DownloadStatus.pending)\n        .toList()\n      ..sort((a, b) => a.key.compareTo(b.key));\n\n    for (final entry in incompleteEpisodes) {\n      await retryDownload(\n        bangumiId: bangumiId,\n        pluginName: pluginName,\n        episodeNumber: entry.key,\n      );\n    }\n\n    if (incompleteEpisodes.isNotEmpty) {\n      KazumiLogger().i(\n        'DownloadController: resumed ${incompleteEpisodes.length} downloads for $recordKey',\n      );\n    }\n  }\n\n  int completedCount(DownloadRecord record) {\n    return record.episodes.values\n        .where((e) => e.status == DownloadStatus.completed)\n        .length;\n  }\n\n}\n\nclass _ResolveRequest {\n  final String recordKey;\n  final int bangumiId;\n  final String pluginName;\n  final int episodeNumber;\n  final String episodePageUrl;\n\n  _ResolveRequest({\n    required this.recordKey,\n    required this.bangumiId,\n    required this.pluginName,\n    required this.episodeNumber,\n    required this.episodePageUrl,\n  });\n}\n"
  },
  {
    "path": "lib/pages/download/download_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'download_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$DownloadController on _DownloadController, Store {\n  late final _$recordsAtom =\n      Atom(name: '_DownloadController.records', context: context);\n\n  @override\n  ObservableList<DownloadRecord> get records {\n    _$recordsAtom.reportRead();\n    return super.records;\n  }\n\n  @override\n  set records(ObservableList<DownloadRecord> value) {\n    _$recordsAtom.reportWrite(value, super.records, () {\n      super.records = value;\n    });\n  }\n\n  late final _$_DownloadControllerActionController =\n      ActionController(name: '_DownloadController', context: context);\n\n  @override\n  void refreshRecords() {\n    final _$actionInfo = _$_DownloadControllerActionController.startAction(\n        name: '_DownloadController.refreshRecords');\n    try {\n      return super.refreshRecords();\n    } finally {\n      _$_DownloadControllerActionController.endAction(_$actionInfo);\n    }\n  }\n\n  @override\n  String toString() {\n    return '''\nrecords: ${records}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/download/download_episode_sheet.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/modules/roads/road_module.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\n\nclass DownloadEpisodeSheet extends StatefulWidget {\n  final int road;\n\n  const DownloadEpisodeSheet({super.key, required this.road});\n\n  @override\n  State<DownloadEpisodeSheet> createState() => _DownloadEpisodeSheetState();\n}\n\nclass _DownloadEpisodeSheetState extends State<DownloadEpisodeSheet> {\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final DownloadController downloadController =\n      Modular.get<DownloadController>();\n\n  final Set<int> _selectedEpisodes = {};\n\n  Road get currentRoadData => videoPageController.roadList[widget.road];\n  int get episodeCount => currentRoadData.data.length;\n\n  @override\n  Widget build(BuildContext context) {\n    final record = downloadController.getRecord(\n      videoPageController.bangumiItem.id,\n      videoPageController.currentPlugin.name,\n    );\n    final downloadedUrls = <String>{};\n    if (record != null) {\n      for (final entry in record.episodes.entries) {\n        if (entry.value.status == DownloadStatus.completed ||\n            entry.value.status == DownloadStatus.downloading ||\n            entry.value.status == DownloadStatus.pending) {\n          if (entry.value.episodePageUrl.isNotEmpty) {\n            downloadedUrls.add(entry.value.episodePageUrl);\n          }\n        }\n      }\n    }\n\n    return DraggableScrollableSheet(\n      initialChildSize: 0.6,\n      minChildSize: 0.3,\n      maxChildSize: 0.9,\n      expand: false,\n      builder: (context, scrollController) {\n        return Column(\n          children: [\n            // Header\n            Padding(\n              padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                children: [\n                  const Text(\n                    '选择要下载的集数',\n                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),\n                  ),\n                  Text(\n                    '已选 ${_selectedEpisodes.length} 集',\n                    style: TextStyle(\n                      fontSize: 14,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            // Select all / deselect all\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              child: Row(\n                children: [\n                  TextButton(\n                    onPressed: () {\n                      setState(() {\n                        _selectedEpisodes.clear();\n                        for (int i = 1; i <= episodeCount; i++) {\n                          final url = currentRoadData.data[i - 1];\n                          if (!downloadedUrls.contains(url)) {\n                            _selectedEpisodes.add(i);\n                          }\n                        }\n                      });\n                    },\n                    child: const Text('全选'),\n                  ),\n                  TextButton(\n                    onPressed: () {\n                      setState(() {\n                        _selectedEpisodes.clear();\n                      });\n                    },\n                    child: const Text('取消全选'),\n                  ),\n                ],\n              ),\n            ),\n            SizedBox(height: 8),\n            // Grid\n            Expanded(\n              child: GridView.builder(\n                controller: scrollController,\n                padding: const EdgeInsets.symmetric(horizontal: 12),\n                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\n                  crossAxisCount: 3,\n                  crossAxisSpacing: 8,\n                  mainAxisSpacing: 8,\n                  mainAxisExtent: 56,\n                ),\n                itemCount: episodeCount,\n                itemBuilder: (context, index) {\n                  final episodeNumber = index + 1;\n                  final episodeUrl = currentRoadData.data[index];\n                  final isDownloaded = downloadedUrls.contains(episodeUrl);\n                  final isSelected = _selectedEpisodes.contains(episodeNumber);\n                  final identifier = currentRoadData.identifier[index];\n\n                  return Material(\n                    color: isDownloaded\n                        ? Theme.of(context)\n                            .colorScheme\n                            .surfaceContainerHighest\n                            .withValues(alpha: 0.5)\n                        : isSelected\n                            ? Theme.of(context).colorScheme.primaryContainer\n                            : Theme.of(context).colorScheme.onInverseSurface,\n                    borderRadius: BorderRadius.circular(8),\n                    clipBehavior: Clip.hardEdge,\n                    child: InkWell(\n                      onTap: isDownloaded\n                          ? null\n                          : () {\n                              setState(() {\n                                if (isSelected) {\n                                  _selectedEpisodes.remove(episodeNumber);\n                                } else {\n                                  _selectedEpisodes.add(episodeNumber);\n                                }\n                              });\n                            },\n                      child: Stack(\n                        children: [\n                          Center(\n                            child: Padding(\n                              padding:\n                                  const EdgeInsets.symmetric(horizontal: 8),\n                              child: Text(\n                                identifier,\n                                maxLines: 2,\n                                overflow: TextOverflow.ellipsis,\n                                textAlign: TextAlign.center,\n                                style: TextStyle(\n                                  fontSize: 13,\n                                  color: isDownloaded\n                                      ? Theme.of(context).colorScheme.outline\n                                      : null,\n                                ),\n                              ),\n                            ),\n                          ),\n                          if (isDownloaded)\n                            Positioned(\n                              top: 4,\n                              right: 4,\n                              child: Icon(\n                                Icons.check_circle,\n                                size: 14,\n                                color: Theme.of(context).colorScheme.outline,\n                              ),\n                            ),\n                          if (isSelected)\n                            Positioned(\n                              top: 4,\n                              right: 4,\n                              child: Icon(\n                                Icons.check_circle,\n                                size: 14,\n                                color: Theme.of(context).colorScheme.primary,\n                              ),\n                            ),\n                        ],\n                      ),\n                    ),\n                  );\n                },\n              ),\n            ),\n            // Action buttons\n            Padding(\n              padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),\n              child: SafeArea(\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.end,\n                  children: [\n                    TextButton(\n                      onPressed: () => Navigator.pop(context),\n                      child: const Text('取消'),\n                    ),\n                    const SizedBox(width: 12),\n                    SizedBox(\n                      width: 140,\n                      child: FilledButton(\n                        onPressed: _selectedEpisodes.isEmpty\n                            ? null\n                            : () => _startBatchDownload(context),\n                        child: Text('开始下载(${_selectedEpisodes.length})'),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _startBatchDownload(BuildContext context) {\n    Navigator.pop(context);\n\n    final plugin = videoPageController.currentPlugin;\n    final bangumiItem = videoPageController.bangumiItem;\n\n    final sortedEpisodes = _selectedEpisodes.toList()..sort();\n\n    for (final episodeNumber in sortedEpisodes) {\n      final episodePageUrl = currentRoadData.data[episodeNumber - 1];\n      final identifier = currentRoadData.identifier[episodeNumber - 1];\n\n      downloadController.startDownload(\n        bangumiId: bangumiItem.id,\n        bangumiName: bangumiItem.nameCn.isNotEmpty\n            ? bangumiItem.nameCn\n            : bangumiItem.name,\n        bangumiCover: bangumiItem.images['large'] ?? '',\n        pluginName: plugin.name,\n        episodeNumber: episodeNumber,\n        episodeName: identifier,\n        road: widget.road,\n        episodePageUrl: episodePageUrl,\n      );\n    }\n\n    KazumiDialog.showToast(\n      message: '已添加 ${sortedEpisodes.length} 集到下载队列，可在下载管理中查看',\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/download/download_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/utils/format_utils.dart';\n\nclass DownloadPage extends StatefulWidget {\n  const DownloadPage({super.key});\n\n  @override\n  State<DownloadPage> createState() => _DownloadPageState();\n}\n\nclass _DownloadPageState extends State<DownloadPage> {\n  final DownloadController downloadController =\n      Modular.get<DownloadController>();\n\n  @override\n  void initState() {\n    super.initState();\n    downloadController.refreshRecords();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: const SysAppBar(title: Text('下载管理')),\n      body: Observer(builder: (context) {\n        if (downloadController.records.isEmpty) {\n          return const Center(\n            child: Text('暂无离线下载'),\n          );\n        }\n        return ListView.builder(\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n          itemCount: downloadController.records.length,\n          itemBuilder: (context, index) {\n            final record = downloadController.records[index];\n            return _buildRecordCard(record);\n          },\n        );\n      }),\n    );\n  }\n\n  Widget _buildRecordCard(DownloadRecord record) {\n    final episodes = record.episodes.values.toList()\n      ..sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber));\n    final completedCount = downloadController.completedCount(record);\n\n    return Card(\n      margin: const EdgeInsets.only(bottom: 12),\n      clipBehavior: Clip.antiAlias,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          // Header\n          ListTile(\n            leading: ClipRRect(\n              borderRadius: BorderRadius.circular(4),\n              child: Image.network(\n                record.bangumiCover,\n                width: 48,\n                height: 64,\n                fit: BoxFit.cover,\n                errorBuilder: (_, __, ___) => Container(\n                  width: 48,\n                  height: 64,\n                  color: Theme.of(context).colorScheme.surfaceContainerHighest,\n                  child: const Icon(Icons.movie_outlined),\n                ),\n              ),\n            ),\n            title: Text(\n              record.bangumiName,\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n            ),\n            subtitle: Text(\n              '来源: ${record.pluginName} · $completedCount/${episodes.length} 已完成',\n              style: TextStyle(\n                fontSize: 12,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n            trailing: PopupMenuButton<String>(\n              onSelected: (value) {\n                if (value == 'delete') {\n                  _confirmDeleteRecord(record);\n                } else if (value == 'resume_all') {\n                  downloadController.resumeAllDownloads(\n                    record.bangumiId,\n                    record.pluginName,\n                  );\n                  KazumiDialog.showToast(message: '已开始恢复下载');\n                }\n              },\n              itemBuilder: (context) => [\n                const PopupMenuItem(\n                  value: 'resume_all',\n                  child: Text('开始全部'),\n                ),\n                const PopupMenuItem(\n                  value: 'delete',\n                  child: Text('删除全部'),\n                ),\n              ],\n            ),\n          ),\n          // Episode list\n          ...episodes.map((ep) => _buildEpisodeTile(record, ep)),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildEpisodeTile(DownloadRecord record, DownloadEpisode episode) {\n    final statusIcon = _getStatusIcon(episode);\n    final statusText = _getStatusText(record, episode);\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),\n      child: Row(\n        children: [\n          statusIcon,\n          const SizedBox(width: 8),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  episode.episodeName.isNotEmpty\n                      ? episode.episodeName\n                      : '第${episode.episodeNumber}集',\n                  maxLines: 1,\n                  overflow: TextOverflow.ellipsis,\n                  style: const TextStyle(fontSize: 14),\n                ),\n                const SizedBox(height: 2),\n                Text(\n                  statusText,\n                  style: TextStyle(\n                    fontSize: 12,\n                    color: episode.status == DownloadStatus.failed\n                        ? Theme.of(context).colorScheme.error\n                        : Theme.of(context).colorScheme.outline,\n                  ),\n                ),\n              ],\n            ),\n          ),\n          if (episode.status == DownloadStatus.downloading) ...[\n            SizedBox(\n              width: 60,\n              child: LinearProgressIndicator(\n                value: episode.progressPercent,\n                minHeight: 3,\n              ),\n            ),\n            const SizedBox(width: 8),\n          ],\n          ..._getActionButtons(record, episode),\n        ],\n      ),\n    );\n  }\n\n  Widget _getStatusIcon(DownloadEpisode episode) {\n    switch (episode.status) {\n      case DownloadStatus.completed:\n        return Icon(Icons.offline_pin,\n            size: 20, color: Theme.of(context).colorScheme.primary);\n      case DownloadStatus.downloading:\n        return SizedBox(\n          width: 20,\n          height: 20,\n          child: CircularProgressIndicator(\n            value: episode.progressPercent,\n            strokeWidth: 2,\n          ),\n        );\n      case DownloadStatus.failed:\n        return Icon(Icons.error_outline,\n            size: 20, color: Theme.of(context).colorScheme.error);\n      case DownloadStatus.paused:\n        return Icon(Icons.pause_circle_outline,\n            size: 20, color: Theme.of(context).colorScheme.outline);\n      case DownloadStatus.pending:\n        return Icon(Icons.hourglass_empty,\n            size: 20, color: Theme.of(context).colorScheme.outline);\n      case DownloadStatus.resolving:\n        return const SizedBox(\n          width: 20,\n          height: 20,\n          child: CircularProgressIndicator(strokeWidth: 2),\n        );\n      default:\n        return const SizedBox(width: 20, height: 20);\n    }\n  }\n\n  String _getStatusText(DownloadRecord record, DownloadEpisode episode) {\n    switch (episode.status) {\n      case DownloadStatus.completed:\n        return '已完成  ${formatBytes(episode.totalBytes)}';\n      case DownloadStatus.downloading:\n        final speed = downloadController.getSpeed(\n          record.bangumiId,\n          record.pluginName,\n          episode.episodeNumber,\n        );\n        final speedText = speed > 0 ? ' · ${formatSpeed(speed)}' : '';\n        return '${(episode.progressPercent * 100).toStringAsFixed(0)}%  '\n            '${episode.downloadedSegments}/${episode.totalSegments}$speedText';\n      case DownloadStatus.failed:\n        return episode.errorMessage.isNotEmpty ? episode.errorMessage : '下载失败';\n      case DownloadStatus.paused:\n        return '已暂停  ${(episode.progressPercent * 100).toStringAsFixed(0)}%';\n      case DownloadStatus.pending:\n        return '等待中';\n      case DownloadStatus.resolving:\n        return '解析视频源中';\n      default:\n        return '';\n    }\n  }\n\n  List<Widget> _getActionButtons(\n      DownloadRecord record, DownloadEpisode episode) {\n    final buttons = <Widget>[];\n\n    switch (episode.status) {\n      case DownloadStatus.completed:\n        buttons.add(IconButton(\n          icon: Icon(Icons.play_circle_outline,\n              size: 20, color: Theme.of(context).colorScheme.primary),\n          onPressed: () => _playEpisode(record, episode),\n          tooltip: '播放',\n          visualDensity: VisualDensity.compact,\n        ));\n        break;\n      case DownloadStatus.downloading:\n        buttons.add(IconButton(\n          icon: const Icon(Icons.pause, size: 20),\n          onPressed: () => downloadController.pauseDownload(\n            record.bangumiId,\n            record.pluginName,\n            episode.episodeNumber,\n          ),\n          tooltip: '暂停',\n          visualDensity: VisualDensity.compact,\n        ));\n        break;\n      case DownloadStatus.paused:\n        buttons.add(IconButton(\n          icon: const Icon(Icons.play_arrow, size: 20),\n          onPressed: () => downloadController.retryDownload(\n            bangumiId: record.bangumiId,\n            pluginName: record.pluginName,\n            episodeNumber: episode.episodeNumber,\n          ),\n          tooltip: '继续',\n          visualDensity: VisualDensity.compact,\n        ));\n        break;\n      case DownloadStatus.failed:\n        buttons.add(IconButton(\n          icon: const Icon(Icons.refresh, size: 20),\n          onPressed: () => downloadController.retryDownload(\n            bangumiId: record.bangumiId,\n            pluginName: record.pluginName,\n            episodeNumber: episode.episodeNumber,\n          ),\n          tooltip: '重试',\n          visualDensity: VisualDensity.compact,\n        ));\n        break;\n      case DownloadStatus.pending:\n        buttons.add(IconButton(\n          icon: Icon(Icons.priority_high,\n              size: 20, color: Theme.of(context).colorScheme.primary),\n          onPressed: () {\n            downloadController.priorityDownload(\n              bangumiId: record.bangumiId,\n              pluginName: record.pluginName,\n              episodeNumber: episode.episodeNumber,\n            );\n            KazumiDialog.showToast(message: '已插队优先下载');\n          },\n          tooltip: '优先下载',\n          visualDensity: VisualDensity.compact,\n        ));\n        break;\n      default:\n        break;\n    }\n\n    buttons.add(IconButton(\n      icon: const Icon(Icons.delete_outline, size: 20),\n      onPressed: () => _confirmDeleteEpisode(record, episode),\n      tooltip: '删除',\n      visualDensity: VisualDensity.compact,\n    ));\n\n    return buttons;\n  }\n\n  void _playEpisode(DownloadRecord record, DownloadEpisode episode) {\n    final localPath = downloadController.getLocalVideoPath(\n      record.bangumiId,\n      record.pluginName,\n      episode.episodeNumber,\n    );\n    if (localPath == null) {\n      KazumiDialog.showToast(message: '本地文件不存在');\n      return;\n    }\n\n    // 构建 BangumiItem\n    final bangumiItem = BangumiItem(\n      id: record.bangumiId,\n      type: 2,\n      name: record.bangumiName,\n      nameCn: record.bangumiName,\n      summary: '',\n      airDate: '',\n      airWeekday: 0,\n      rank: 0,\n      images: {'large': record.bangumiCover},\n      tags: [],\n      alias: [],\n      ratingScore: 0.0,\n      votes: 0,\n      votesCount: [],\n      info: '',\n    );\n\n    // 获取所有已下载集数（通过 Controller 委托给 Repository 层）\n    final downloadedEpisodes = downloadController.getCompletedEpisodes(\n      record.bangumiId,\n      record.pluginName,\n    );\n\n    // 初始化离线模式\n    final videoPageController = Modular.get<VideoPageController>();\n    videoPageController.initForOfflinePlayback(\n      bangumiItem: bangumiItem,\n      pluginName: record.pluginName,\n      episodeNumber: episode.episodeNumber,\n      episodeName: episode.episodeName,\n      road: episode.road,\n      videoPath: localPath,\n      downloadedEpisodes: downloadedEpisodes,\n    );\n\n    // 导航到 VideoPage\n    Modular.to.pushNamed('/video/');\n  }\n\n  void _confirmDeleteEpisode(DownloadRecord record, DownloadEpisode episode) {\n    KazumiDialog.show(\n      builder: (context) => AlertDialog(\n        title: const Text('删除下载'),\n        content: Text('确定要删除「${episode.episodeName.isNotEmpty ? episode.episodeName : '第${episode.episodeNumber}集'}」的下载文件吗？'),\n        actions: [\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () {\n              downloadController.deleteEpisode(\n                record.bangumiId,\n                record.pluginName,\n                episode.episodeNumber,\n              );\n              KazumiDialog.dismiss();\n            },\n            child: const Text('删除'),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _confirmDeleteRecord(DownloadRecord record) {\n    KazumiDialog.show(\n      builder: (context) => AlertDialog(\n        title: const Text('删除全部下载'),\n        content: Text('确定要删除「${record.bangumiName}」的所有下载文件吗？'),\n        actions: [\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () {\n              downloadController.deleteRecord(\n                record.bangumiId,\n                record.pluginName,\n              );\n              KazumiDialog.dismiss();\n            },\n            child: const Text('删除'),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/download/download_page_module.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/download/download_page.dart';\n\nclass DownloadModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const DownloadPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/error/storage_error_page.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'package:path_provider/path_provider.dart';\n\nclass StorageErrorPage extends StatelessWidget {\n  const StorageErrorPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('内部错误'),\n      ),\n      body: Center(\n        child: FutureBuilder<Directory>(\n          future: getApplicationSupportDirectory(),\n          builder: (context, snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              final supportDir = snapshot.data;\n              final path = supportDir != null ? '$supportDir' : '未知路径';\n              return GeneralErrorWidget(\n                errMsg: '存储初始化错误 \\n 当前储存位置 $path \\n 尝试删除该目录以重置本地存储',\n                actions: [\n                  GeneralErrorButton(\n                    onPressed: () {\n                      exit(0);\n                    },\n                    text: '退出程序',\n                  ),\n                ],\n              );\n            } else {\n              return const CircularProgressIndicator();\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/history_controller.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/history/history_module.dart';\nimport 'package:kazumi/repositories/history_repository.dart';\nimport 'package:mobx/mobx.dart';\n\npart 'history_controller.g.dart';\n\nclass HistoryController = _HistoryController with _$HistoryController;\n\nabstract class _HistoryController with Store {\n  final _historyRepository = Modular.get<IHistoryRepository>();\n\n  @observable\n  ObservableList<History> histories = ObservableList<History>(); \n\n  void init() {\n    final temp = _historyRepository.getAllHistories();\n    histories.clear();\n    histories.addAll(temp);\n  }\n\n  Future<void> updateHistory(\n      int episode, int road, String adapterName, BangumiItem bangumiItem, Duration progress, String lastSrc, String lastWatchEpisodeName) async {\n    await _historyRepository.updateHistory(\n      episode: episode,\n      road: road,\n      adapterName: adapterName,\n      bangumiItem: bangumiItem,\n      progress: progress,\n      lastSrc: lastSrc,\n      lastWatchEpisodeName: lastWatchEpisodeName,\n    );\n    init();\n  }\n\n  Progress? lastWatching(BangumiItem bangumiItem, String adapterName) {\n    return _historyRepository.getLastWatchingProgress(bangumiItem, adapterName);\n  }\n\n  Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode) {\n    return _historyRepository.findProgress(bangumiItem, adapterName, episode);\n  }\n\n  Future<void> deleteHistory(History history) async {\n    await _historyRepository.deleteHistory(history);\n    init();\n  }\n\n  Future<void> clearProgress(BangumiItem bangumiItem, String adapterName, int episode) async {\n    await _historyRepository.clearProgress(bangumiItem, adapterName, episode);\n    init();\n  }\n\n  Future<void> clearAll() async {\n    await _historyRepository.clearAllHistories();\n    histories.clear();\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/history_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'history_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$HistoryController on _HistoryController, Store {\n  late final _$historiesAtom =\n      Atom(name: '_HistoryController.histories', context: context);\n\n  @override\n  ObservableList<History> get histories {\n    _$historiesAtom.reportRead();\n    return super.histories;\n  }\n\n  @override\n  set histories(ObservableList<History> value) {\n    _$historiesAtom.reportWrite(value, super.histories, () {\n      super.histories = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\nhistories: ${histories}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/history_module.dart",
    "content": "import 'package:kazumi/pages/history/history_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass HistoryModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const HistoryPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/history/history_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/bean/card/bangumi_history_card.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/utils/constants.dart';\n\nclass HistoryPage extends StatefulWidget {\n  const HistoryPage({super.key});\n\n  @override\n  State<HistoryPage> createState() => _HistoryPageState();\n}\n\nclass _HistoryPageState extends State<HistoryPage>\n    with SingleTickerProviderStateMixin {\n  final HistoryController historyController = Modular.get<HistoryController>();\n\n  /// show delete button\n  bool showDelete = false;\n\n  @override\n  void initState() {\n    super.initState();\n    historyController.init();\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  void showHistoryClearDialog() {\n    KazumiDialog.show(\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('记录管理'),\n          content: const Text('确认要清除所有历史记录吗?'),\n          actions: [\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n              },\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n                try {\n                  historyController.clearAll();\n                } catch (_) {}\n              },\n              child: const Text('确认'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {});\n    return Observer(builder: (context) {\n      return PopScope(\n        canPop: true,\n        onPopInvokedWithResult: (bool didPop, Object? result) async {\n          onBackPressed(context);\n        },\n        child: Scaffold(\n          appBar: SysAppBar(\n            title: const Text('历史记录'),\n            actions: [\n              IconButton(\n                  onPressed: () {\n                    setState(() {\n                      showDelete = !showDelete;\n                    });\n                  },\n                  icon: showDelete\n                      ? const Icon(Icons.edit_outlined)\n                      : const Icon(Icons.edit))\n            ],\n          ),\n          body: SafeArea(bottom: false, child: renderBody),\n          floatingActionButton: FloatingActionButton(\n            child: const Icon(Icons.clear_all),\n            onPressed: () {\n              showHistoryClearDialog();\n            },\n          ),\n        ),\n      );\n    });\n  }\n\n  Widget get renderBody {\n    if (historyController.histories.isNotEmpty) {\n      return contentGrid;\n    } else {\n      return const Center(\n        child: Text('没有找到历史记录 (´;ω;`)'),\n      );\n    }\n  }\n\n  Widget get contentGrid {\n    int crossCount = 1;\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {\n      crossCount = 2;\n    }\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {\n      crossCount = 3;\n    }\n    double cardHeight = 120;\n\n    return CustomScrollView(\n      slivers: [\n        SliverGrid(\n          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n            mainAxisSpacing: StyleString.cardSpace - 2,\n            crossAxisSpacing: StyleString.cardSpace,\n            crossAxisCount: crossCount,\n            mainAxisExtent: cardHeight + 12,\n          ),\n          delegate: SliverChildBuilderDelegate(\n            (BuildContext context, int index) {\n              return historyController.histories.isNotEmpty\n                  ? BangumiHistoryCardV(\n                      showDelete: showDelete,\n                      cardHeight: cardHeight,\n                      historyItem: historyController.histories[index])\n                  : null;\n            },\n            childCount: historyController.histories.isNotEmpty\n                ? historyController.histories.length\n                : 10,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/index_module.dart",
    "content": "import 'package:kazumi/pages/index_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/router.dart';\nimport 'package:kazumi/pages/init_page.dart';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/pages/popular/popular_controller.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/pages/timeline/timeline_controller.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:kazumi/pages/my/my_controller.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/pages/video/video_module.dart';\nimport 'package:kazumi/pages/info/info_module.dart';\nimport 'package:kazumi/pages/settings/settings_module.dart';\nimport 'package:kazumi/shaders/shaders_controller.dart';\nimport 'package:kazumi/pages/search/search_module.dart';\nimport 'package:kazumi/repositories/collect_repository.dart';\nimport 'package:kazumi/repositories/search_history_repository.dart';\nimport 'package:kazumi/repositories/collect_crud_repository.dart';\nimport 'package:kazumi/repositories/history_repository.dart';\nimport 'package:kazumi/repositories/download_repository.dart';\nimport 'package:kazumi/utils/download_manager.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\n\nclass IndexModule extends Module {\n  @override\n  List<Module> get imports => menu.moduleList;\n\n  @override\n  void binds(i) {\n    // Repository层\n    i.addSingleton<ICollectRepository>(CollectRepository.new);\n    i.addSingleton<ISearchHistoryRepository>(SearchHistoryRepository.new);\n    i.addSingleton<ICollectCrudRepository>(CollectCrudRepository.new);\n    i.addSingleton<IHistoryRepository>(HistoryRepository.new);\n    i.addSingleton<IDownloadRepository>(DownloadRepository.new);\n    i.addSingleton<IDownloadManager>(DownloadManager.new);\n\n    // Controller层\n    i.addSingleton(PopularController.new);\n    i.addSingleton(PluginsController.new);\n    i.addSingleton(VideoPageController.new);\n    i.addSingleton(TimelineController.new);\n    i.addSingleton(CollectController.new);\n    i.addSingleton(HistoryController.new);\n    i.addSingleton(MyController.new);\n    i.addSingleton(ShadersController.new);\n    i.addSingleton(DownloadController.new);\n  }\n\n  @override\n  void routes(r) {\n    r.child(\"/\",\n        child: (_) => const InitPage(),\n        children: [\n          ChildRoute(\n            \"/error\",\n            child: (_) => Scaffold(\n              appBar: AppBar(title: const Text(\"Kazumi\")),\n              body: const Center(child: Text(\"初始化失败\")),\n            ),\n          ),\n        ],\n        transition: TransitionType.noTransition);\n    r.child(\n      \"/tab\",\n      child: (_) {\n        return const IndexPage();\n      },\n      children: menu.routes,\n      transition: TransitionType.fadeIn,\n      duration: Duration(milliseconds: 70),\n    );\n    r.module(\"/video\", module: VideoModule());\n    /// The route need [ BangumiItem ] as argument.\n    r.module(\"/info\", module: InfoModule());\n    r.module(\"/settings\", module: SettingsModule());\n    r.module(\"/search\", module: SearchModule());\n  }\n}\n"
  },
  {
    "path": "lib/pages/index_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/pages/menu/menu.dart';\n\n\nclass IndexPage extends StatefulWidget {\n  //const IndexPage({super.key});\n  const IndexPage({super.key});\n\n  @override\n  State<IndexPage> createState() => _IndexPageState();\n}\n\nclass _IndexPageState extends State<IndexPage> with  WidgetsBindingObserver {\n\n  @override\n  Widget build(BuildContext context) {\n    return const ScaffoldMenu();\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/character_page.dart",
    "content": "import 'dart:ui';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/modules/character/character_full_item.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\nimport 'package:kazumi/bean/card/character_comments_card.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\n\nclass CharacterPage extends StatefulWidget {\n  const CharacterPage({super.key, required this.characterID});\n\n  final int characterID;\n\n  @override\n  State<CharacterPage> createState() => _CharacterPageState();\n}\n\nclass _CharacterPageState extends State<CharacterPage> {\n  late CharacterFullItem characterFullItem;\n  bool loadingCharacter = true;\n  List<CharacterCommentItem> commentsList = [];\n  bool loadingComments = true;\n\n  Future<void> loadCharacter() async {\n    setState(() {\n      loadingCharacter = true;\n    });\n    await BangumiHTTP.getCharacterByCharacterID(widget.characterID)\n        .then((character) {\n      characterFullItem = character;\n    });\n    if (mounted) {\n      setState(() {\n        loadingCharacter = false;\n      });\n    }\n  }\n\n  Future<void> loadComments() async {\n    setState(() {\n      loadingComments = true;\n    });\n    await BangumiHTTP.getCharacterCommentsByCharacterID(widget.characterID)\n        .then((value) {\n      commentsList = value.commentList;\n    });\n    if (mounted) {\n      setState(() {\n        loadingComments = false;\n      });\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      loadCharacter();\n      loadComments();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DefaultTabController(\n      length: 2,\n      child: Scaffold(\n        body: Column(\n          children: [\n            const PreferredSize(\n              preferredSize: Size.fromHeight(kToolbarHeight),\n              child: Material(\n                child: TabBar(\n                  tabs: [\n                    Tab(text: '人物资料'),\n                    Tab(text: '吐槽箱'),\n                  ],\n                ),\n              ),\n            ),\n            Expanded(\n              child: TabBarView(\n                children: [characterInfoBody, characterCommentsBody],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget get characterInfoBody {\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: LayoutBuilder(builder: (context, constraints) {\n        return Column(\n          children: [\n            Expanded(\n              child: loadingCharacter\n                  ? const Center(child: CircularProgressIndicator())\n                  : (characterFullItem.id == 0\n                      ? GeneralErrorWidget(\n                          errMsg: '什么都没有找到 (´;ω;`)',\n                          actions: [\n                            GeneralErrorButton(\n                              onPressed: () {\n                                loadCharacter();\n                              },\n                              text: '点击重试',\n                            ),\n                          ],\n                        )\n                      : SizedBox(\n                          width: double.infinity,\n                          child: Row(\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            children: [\n                              SizedBox(\n                                width: constraints.maxWidth * 0.3,\n                                height: constraints.maxHeight,\n                                child: NetworkImgLayer(\n                                  width: constraints.maxWidth,\n                                  height: constraints.maxHeight,\n                                  src: characterFullItem.image,\n                                ),\n                              ),\n                              Expanded(\n                                child: SingleChildScrollView(\n                                  child: Padding(\n                                    padding: const EdgeInsets.all(16.0),\n                                    child: Column(\n                                      crossAxisAlignment:\n                                          CrossAxisAlignment.start,\n                                      children: [\n                                        Text(\n                                          characterFullItem.name,\n                                          style: Theme.of(context)\n                                              .textTheme\n                                              .headlineSmall\n                                              ?.copyWith(\n                                                fontWeight: FontWeight.bold,\n                                                color: Theme.of(context)\n                                                    .colorScheme\n                                                    .tertiary,\n                                              ),\n                                          overflow: TextOverflow.ellipsis,\n                                          maxLines: 2,\n                                        ),\n                                        Padding(\n                                          padding: const EdgeInsets.only(\n                                              top: 4.0, bottom: 12.0),\n                                          child: Text(\n                                            characterFullItem.nameCN,\n                                            style: Theme.of(context)\n                                                .textTheme\n                                                .titleMedium\n                                                ?.copyWith(\n                                                  color: Colors.grey[700],\n                                                ),\n                                          ),\n                                        ),\n                                        const Divider(),\n                                        Padding(\n                                          padding: const EdgeInsets.symmetric(\n                                              vertical: 8.0),\n                                          child: Text(\n                                            '基本信息',\n                                            style: Theme.of(context)\n                                                .textTheme\n                                                .titleSmall\n                                                ?.copyWith(\n                                                  fontWeight: FontWeight.bold,\n                                                ),\n                                          ),\n                                        ),\n                                        Text(\n                                          characterFullItem.info,\n                                          style: Theme.of(context)\n                                              .textTheme\n                                              .bodyMedium,\n                                          textAlign: TextAlign.justify,\n                                        ),\n                                        const SizedBox(height: 16.0),\n                                        Padding(\n                                          padding: const EdgeInsets.symmetric(\n                                              vertical: 8.0),\n                                          child: Text(\n                                            '角色简介',\n                                            style: Theme.of(context)\n                                                .textTheme\n                                                .titleSmall\n                                                ?.copyWith(\n                                                  fontWeight: FontWeight.bold,\n                                                ),\n                                          ),\n                                        ),\n                                        Text(\n                                          characterFullItem.summary,\n                                          style: Theme.of(context)\n                                              .textTheme\n                                              .bodyMedium,\n                                          textAlign: TextAlign.justify,\n                                        ),\n                                      ],\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ],\n                          ),\n                        )),\n            ),\n          ],\n        );\n      }),\n    );\n  }\n\n  Widget get characterCommentsBody {\n    return CustomScrollView(\n      scrollBehavior: const ScrollBehavior().copyWith(\n        // Scrollbars' movement is not linear so hide it.\n        scrollbars: false,\n        // Enable mouse drag to refresh\n        dragDevices: {\n          PointerDeviceKind.mouse,\n          PointerDeviceKind.touch,\n        },\n      ),\n      slivers: [\n        SliverPadding(\n          padding: const EdgeInsets.fromLTRB(4, 0, 4, 4),\n          sliver: Builder(builder: (context) {\n            if (loadingComments) {\n              return const SliverFillRemaining(\n                child: Center(\n                  child: CircularProgressIndicator(),\n                ),\n              );\n            }\n            if (commentsList.isEmpty) {\n              return SliverFillRemaining(\n                child: GeneralErrorWidget(\n                  errMsg: '什么都没有找到 (´;ω;`)',\n                  actions: [\n                    GeneralErrorButton(\n                      onPressed: () {\n                        loadComments();\n                      },\n                      text: '点击重试',\n                    ),\n                  ],\n                ),\n              );\n            }\n            return SliverList(\n              delegate: SliverChildBuilderDelegate(\n                (context, index) {\n                  // Fix scroll issue caused by height change of network images\n                  // by keeping loaded cards alive.\n                  return KeepAlive(\n                    keepAlive: true,\n                    child: IndexedSemantics(\n                      index: index,\n                      child: SelectionArea(\n                        child: CharacterCommentsCard(\n                          commentItem: commentsList[index],\n                        ),\n                      ),\n                    ),\n                  );\n                },\n                childCount: commentsList.length,\n                addAutomaticKeepAlives: false,\n                addRepaintBoundaries: false,\n                addSemanticIndexes: false,\n              ),\n            );\n          }),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/info_controller.dart",
    "content": "import 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/modules/search/plugin_search_module.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/modules/characters/character_item.dart';\nimport 'package:kazumi/modules/staff/staff_item.dart';\n\npart 'info_controller.g.dart';\n\nclass InfoController = _InfoController with _$InfoController;\n\nabstract class _InfoController with Store {\n  final CollectController collectController = Modular.get<CollectController>();\n  late BangumiItem bangumiItem;\n\n  @observable\n  bool isLoading = false;\n\n  @observable\n  var pluginSearchResponseList = ObservableList<PluginSearchResponse>();\n\n  @observable\n  var pluginSearchStatus = ObservableMap<String, String>();\n\n  @observable\n  var commentsList = ObservableList<CommentItem>();\n\n  @observable\n  var characterList = ObservableList<CharacterItem>();\n\n  @observable\n  var staffList = ObservableList<StaffFullItem>();\n\n  Future<void> queryBangumiInfoByID(int id, {String type = \"init\"}) async {\n    isLoading = true;\n    await BangumiHTTP.getBangumiInfoByID(id).then((value) {\n      if (value != null) {\n        if (type == \"init\") {\n          bangumiItem = value;\n        } else {\n          bangumiItem.summary = value.summary;\n          bangumiItem.tags = value.tags;\n          bangumiItem.rank = value.rank;\n          bangumiItem.airDate = value.airDate;\n          bangumiItem.airWeekday = value.airWeekday;\n          bangumiItem.alias = value.alias;\n          bangumiItem.ratingScore = value.ratingScore;\n          bangumiItem.votes = value.votes;\n          bangumiItem.votesCount = value.votesCount;\n        }\n        collectController.updateLocalCollect(bangumiItem);\n        isLoading = false;\n      }\n    });\n  }\n\n  Future<void> queryBangumiCommentsByID(int id, {int offset = 0}) async {\n    if (offset == 0) {\n      commentsList.clear();\n    }\n    await BangumiHTTP.getBangumiCommentsByID(id, offset: offset).then((value) {\n      commentsList.addAll(value.commentList);\n    });\n    KazumiLogger().i('InfoController: loaded comments list length ${commentsList.length}');\n  }\n\n  Future<void> queryBangumiCharactersByID(int id) async {\n    characterList.clear();\n    await BangumiHTTP.getCharatersByBangumiID(id).then((value) {\n      characterList.addAll(value.charactersList);\n    });\n    Map<String, int> relationValue = {\n      '主角': 1,\n      '配角': 2,\n      '客串': 3,\n    };\n\n    try {\n      characterList.sort((a, b) {\n        int valueA = relationValue[a.relation] ?? 4;\n        int valueB = relationValue[b.relation] ?? 4;\n        return valueA.compareTo(valueB);\n      });\n    } catch (e) {\n      KazumiDialog.showToast(message: '$e');\n    }\n    KazumiLogger().i('InfoController: loaded character list length ${characterList.length}');\n  }\n\n  Future<void> queryBangumiStaffsByID(int id) async {\n    staffList.clear();\n    await BangumiHTTP.getBangumiStaffByID(id).then((value) {\n      staffList.addAll(value.data);\n    });\n    KazumiLogger().i('InfoController: loaded staff list length ${staffList.length}');\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/info_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'info_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$InfoController on _InfoController, Store {\n  late final _$isLoadingAtom =\n      Atom(name: '_InfoController.isLoading', context: context);\n\n  @override\n  bool get isLoading {\n    _$isLoadingAtom.reportRead();\n    return super.isLoading;\n  }\n\n  @override\n  set isLoading(bool value) {\n    _$isLoadingAtom.reportWrite(value, super.isLoading, () {\n      super.isLoading = value;\n    });\n  }\n\n  late final _$pluginSearchResponseListAtom =\n      Atom(name: '_InfoController.pluginSearchResponseList', context: context);\n\n  @override\n  ObservableList<PluginSearchResponse> get pluginSearchResponseList {\n    _$pluginSearchResponseListAtom.reportRead();\n    return super.pluginSearchResponseList;\n  }\n\n  @override\n  set pluginSearchResponseList(ObservableList<PluginSearchResponse> value) {\n    _$pluginSearchResponseListAtom\n        .reportWrite(value, super.pluginSearchResponseList, () {\n      super.pluginSearchResponseList = value;\n    });\n  }\n\n  late final _$pluginSearchStatusAtom =\n      Atom(name: '_InfoController.pluginSearchStatus', context: context);\n\n  @override\n  ObservableMap<String, String> get pluginSearchStatus {\n    _$pluginSearchStatusAtom.reportRead();\n    return super.pluginSearchStatus;\n  }\n\n  @override\n  set pluginSearchStatus(ObservableMap<String, String> value) {\n    _$pluginSearchStatusAtom.reportWrite(value, super.pluginSearchStatus, () {\n      super.pluginSearchStatus = value;\n    });\n  }\n\n  late final _$commentsListAtom =\n      Atom(name: '_InfoController.commentsList', context: context);\n\n  @override\n  ObservableList<CommentItem> get commentsList {\n    _$commentsListAtom.reportRead();\n    return super.commentsList;\n  }\n\n  @override\n  set commentsList(ObservableList<CommentItem> value) {\n    _$commentsListAtom.reportWrite(value, super.commentsList, () {\n      super.commentsList = value;\n    });\n  }\n\n  late final _$characterListAtom =\n      Atom(name: '_InfoController.characterList', context: context);\n\n  @override\n  ObservableList<CharacterItem> get characterList {\n    _$characterListAtom.reportRead();\n    return super.characterList;\n  }\n\n  @override\n  set characterList(ObservableList<CharacterItem> value) {\n    _$characterListAtom.reportWrite(value, super.characterList, () {\n      super.characterList = value;\n    });\n  }\n\n  late final _$staffListAtom =\n      Atom(name: '_InfoController.staffList', context: context);\n\n  @override\n  ObservableList<StaffFullItem> get staffList {\n    _$staffListAtom.reportRead();\n    return super.staffList;\n  }\n\n  @override\n  set staffList(ObservableList<StaffFullItem> value) {\n    _$staffListAtom.reportWrite(value, super.staffList, () {\n      super.staffList = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\nisLoading: ${isLoading},\npluginSearchResponseList: ${pluginSearchResponseList},\npluginSearchStatus: ${pluginSearchStatus},\ncommentsList: ${commentsList},\ncharacterList: ${characterList},\nstaffList: ${staffList}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/info_module.dart",
    "content": "import 'package:kazumi/pages/info/info_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass InfoModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const InfoPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/info_page.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/pages/info/info_controller.dart';\nimport 'package:kazumi/bean/card/bangumi_info_card.dart';\nimport 'package:kazumi/pages/info/source_sheet.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/bean/card/network_img_layer.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/pages/info/info_tabview.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb;\n\nclass InfoPage extends StatefulWidget {\n  const InfoPage({super.key});\n\n  @override\n  State<InfoPage> createState() => _InfoPageState();\n}\n\nclass _InfoPageState extends State<InfoPage> with TickerProviderStateMixin {\n  /// Don't use modular singleton here. We may have multiple info pages.\n  /// Use a new instance of InfoController for each info page.\n  final InfoController infoController = InfoController();\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  late TabController sourceTabController;\n  late TabController infoTabController;\n  late bool showRating;\n\n  bool commentsIsLoading = false;\n  bool charactersIsLoading = false;\n  bool commentsQueryTimeout = false;\n  bool charactersQueryTimeout = false;\n  bool staffIsLoading = false;\n  bool staffQueryTimeout = false;\n\n  final inputBangumiIten = Modular.args.data as BangumiItem;\n\n  Future<void> loadCharacters() async {\n    if (charactersIsLoading) return;\n    setState(() {\n      charactersIsLoading = true;\n      charactersQueryTimeout = false;\n    });\n    infoController\n        .queryBangumiCharactersByID(infoController.bangumiItem.id)\n        .then((_) {\n      if (infoController.characterList.isEmpty && mounted) {\n        setState(() {\n          charactersIsLoading = false;\n          charactersQueryTimeout = true;\n        });\n      }\n      if (infoController.characterList.isNotEmpty && mounted) {\n        setState(() {\n          charactersIsLoading = false;\n        });\n      }\n    });\n  }\n\n  Future<void> loadStaff() async {\n    if (staffIsLoading) return;\n    setState(() {\n      staffIsLoading = true;\n      staffQueryTimeout = false;\n    });\n    infoController\n        .queryBangumiStaffsByID(infoController.bangumiItem.id)\n        .then((_) {\n      if (infoController.staffList.isEmpty && mounted) {\n        setState(() {\n          staffIsLoading = false;\n          staffQueryTimeout = true;\n        });\n      }\n      if (infoController.staffList.isNotEmpty && mounted) {\n        setState(() {\n          staffIsLoading = false;\n        });\n      }\n    });\n  }\n\n  Future<void> loadMoreComments({int offset = 0}) async {\n    if (commentsIsLoading) return;\n    setState(() {\n      commentsIsLoading = true;\n      commentsQueryTimeout = false;\n    });\n    infoController\n        .queryBangumiCommentsByID(infoController.bangumiItem.id, offset: offset)\n        .then((_) {\n      if (infoController.commentsList.isEmpty && mounted) {\n        setState(() {\n          commentsIsLoading = false;\n          commentsQueryTimeout = true;\n        });\n      }\n      if (infoController.commentsList.isNotEmpty && mounted) {\n        setState(() {\n          commentsIsLoading = false;\n        });\n      }\n    });\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    infoController.bangumiItem = inputBangumiIten;\n    infoController.characterList.clear();\n    infoController.commentsList.clear();\n    infoController.staffList.clear();\n    infoController.pluginSearchResponseList.clear();\n    videoPageController.currentEpisode = 1;\n    // Because the gap between different bangumi API response is too large, sometimes we need to query the bangumi info again\n    // We need the type parameter to determine whether to attach the new data to the old data\n    // We can't generally replace the old data with the new data, because the old data contains images url, update them will cause the image to reload and flicker\n    if (infoController.bangumiItem.summary == '' ||\n        infoController.bangumiItem.votesCount.isEmpty) {\n      queryBangumiInfoByID(infoController.bangumiItem.id, type: 'attach');\n    }\n    sourceTabController =\n        TabController(length: pluginsController.pluginList.length, vsync: this);\n    infoTabController = TabController(length: 5, vsync: this);\n    showRating = GStorage.setting.get(SettingBoxKey.showRating, defaultValue: true);\n    infoTabController.addListener(() {\n      int index = infoTabController.index;\n      if (index == 1 &&\n          infoController.commentsList.isEmpty &&\n          !commentsIsLoading) {\n        loadMoreComments();\n      }\n      if (index == 2 &&\n          infoController.characterList.isEmpty &&\n          !charactersIsLoading) {\n        loadCharacters();\n      }\n      if (index == 4 && infoController.staffList.isEmpty && !staffIsLoading) {\n        loadStaff();\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    infoController.characterList.clear();\n    infoController.commentsList.clear();\n    infoController.staffList.clear();\n    infoController.pluginSearchResponseList.clear();\n    videoPageController.currentEpisode = 1;\n    sourceTabController.dispose();\n    infoTabController.dispose();\n    super.dispose();\n  }\n\n  Future<void> queryBangumiInfoByID(int id, {String type = \"init\"}) async {\n    try {\n      await infoController.queryBangumiInfoByID(id, type: type);\n      setState(() {});\n    } catch (e) {\n      KazumiLogger().e('InfoController: failed to query bangumi info by ID', error: e);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final List<String> tabs = <String>['概览', '吐槽', '角色', '评论', '制作人员'];\n    final bool showWindowButton = GStorage.setting\n        .get(SettingBoxKey.showWindowButton, defaultValue: false);\n    return PopScope(\n      canPop: true,\n      child: DefaultTabController(\n        length: tabs.length,\n        child: Scaffold(\n          body: NestedScrollView(\n            headerSliverBuilder:\n                (BuildContext context, bool innerBoxIsScrolled) {\n              return <Widget>[\n                SliverOverlapAbsorber(\n                  handle:\n                      NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n                  sliver: SliverAppBar.medium(\n                    title: EmbeddedNativeControlArea(\n                      child: dtb.DragToMoveArea(\n                        child: Container(\n                          width: double.infinity,\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                            infoController.bangumiItem.nameCn == ''\n                                ? infoController.bangumiItem.name\n                                : infoController.bangumiItem.nameCn,\n                          ),\n                        ),\n                      ),\n                    ),\n                    automaticallyImplyLeading: false,\n                    scrolledUnderElevation: 0.0,\n                    leading: EmbeddedNativeControlArea(\n                      child: IconButton(\n                        onPressed: () {\n                          Navigator.maybePop(context);\n                        },\n                        icon: Icon(Icons.arrow_back),\n                      ),\n                    ),\n                    actions: [\n                      if (innerBoxIsScrolled)\n                        EmbeddedNativeControlArea(\n                          child: CollectButton(\n                            bangumiItem: infoController.bangumiItem,\n                            color:\n                                Theme.of(context).colorScheme.onSurfaceVariant,\n                          ),\n                        ),\n                      EmbeddedNativeControlArea(\n                        child: IconButton(\n                          onPressed: () {\n                            launchUrl(\n                              Uri.parse(\n                                  'https://bangumi.tv/subject/${infoController.bangumiItem.id}'),\n                              mode: LaunchMode.externalApplication,\n                            );\n                          },\n                          icon: const Icon(Icons.open_in_browser_rounded),\n                        ),\n                      ),\n                      if (!showWindowButton && Utils.isDesktop())\n                        CloseButton(onPressed: () => windowManager.close()),\n                      SizedBox(width: 8),\n                    ],\n                    toolbarHeight: (Platform.isMacOS && showWindowButton)\n                        ? kToolbarHeight + 22\n                        : kToolbarHeight,\n                    stretch: true,\n                    centerTitle: false,\n                    expandedHeight: (Platform.isMacOS && showWindowButton)\n                        ? 308 + kTextTabBarHeight + kToolbarHeight + 22\n                        : 308 + kTextTabBarHeight + kToolbarHeight,\n                    collapsedHeight: (Platform.isMacOS && showWindowButton)\n                        ? kTextTabBarHeight +\n                            kToolbarHeight +\n                            MediaQuery.paddingOf(context).top +\n                            22\n                        : kTextTabBarHeight +\n                            kToolbarHeight +\n                            MediaQuery.paddingOf(context).top,\n                    flexibleSpace: FlexibleSpaceBar(\n                      collapseMode: CollapseMode.pin,\n                      background: Observer(builder: (context) {\n                        return Stack(\n                          children: [\n                            // No background image when loading to make loading looks better\n                            if (!infoController.isLoading)\n                              Positioned.fill(\n                                bottom: kTextTabBarHeight,\n                                child: IgnorePointer(\n                                  child: Opacity(\n                                    opacity: 0.4,\n                                    child: LayoutBuilder(\n                                      builder: (context, boxConstraints) {\n                                        return ImageFiltered(\n                                          imageFilter: ImageFilter.blur(\n                                              sigmaX: 15.0, sigmaY: 15.0),\n                                          child: ShaderMask(\n                                            shaderCallback: (Rect bounds) {\n                                              return const LinearGradient(\n                                                begin: Alignment.topCenter,\n                                                end: Alignment.bottomCenter,\n                                                colors: [\n                                                  Colors.white,\n                                                  Colors.transparent,\n                                                ],\n                                                stops: [0.8, 1],\n                                              ).createShader(bounds);\n                                            },\n                                            child: NetworkImgLayer(\n                                              src: infoController.bangumiItem\n                                                      .images['large'] ??\n                                                  '',\n                                              width: boxConstraints.maxWidth,\n                                              height: boxConstraints.maxHeight,\n                                              fadeInDuration: const Duration(\n                                                  milliseconds: 0),\n                                              fadeOutDuration: const Duration(\n                                                  milliseconds: 0),\n                                            ),\n                                          ),\n                                        );\n                                      },\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            SafeArea(\n                              bottom: false,\n                              child: EmbeddedNativeControlArea(\n                                child: Align(\n                                  alignment: Alignment.topCenter,\n                                  child: Padding(\n                                    padding: const EdgeInsets.fromLTRB(\n                                        16, kToolbarHeight, 16, 0),\n                                    child: BangumiInfoCardV(\n                                      bangumiItem: infoController.bangumiItem,\n                                      isLoading: infoController.isLoading,\n                                      showRating: showRating,\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ],\n                        );\n                      }),\n                    ),\n                    forceElevated: innerBoxIsScrolled,\n                    bottom: TabBar(\n                      controller: infoTabController,\n                      isScrollable: true,\n                      tabAlignment: TabAlignment.center,\n                      dividerHeight: 0,\n                      tabs: tabs.map((name) => Tab(text: name)).toList(),\n                    ),\n                  ),\n                ),\n              ];\n            },\n            body: Observer(builder: (context) {\n              return InfoTabView(\n                tabController: infoTabController,\n                bangumiItem: infoController.bangumiItem,\n                commentsQueryTimeout: commentsQueryTimeout,\n                charactersQueryTimeout: charactersQueryTimeout,\n                staffQueryTimeout: staffQueryTimeout,\n                loadMoreComments: loadMoreComments,\n                loadCharacters: loadCharacters,\n                loadStaff: loadStaff,\n                commentsList: infoController.commentsList,\n                characterList: infoController.characterList,\n                staffList: infoController.staffList,\n                isLoading: infoController.isLoading,\n              );\n            }),\n          ),\n          floatingActionButton: FloatingActionButton.extended(\n            icon: const Icon(Icons.play_arrow_rounded),\n            label: Text('开始观看'),\n            onPressed: () async {\n              showModalBottomSheet(\n                isScrollControlled: true,\n                constraints: BoxConstraints(\n                  maxHeight: (MediaQuery.sizeOf(context).height >=\n                          LayoutBreakpoint.compact['height']!)\n                      ? MediaQuery.of(context).size.height * 3 / 4\n                      : MediaQuery.of(context).size.height,\n                  maxWidth: (MediaQuery.sizeOf(context).width >=\n                          LayoutBreakpoint.medium['width']!)\n                      ? MediaQuery.of(context).size.width * 9 / 16\n                      : MediaQuery.of(context).size.width,\n                ),\n                clipBehavior: Clip.antiAlias,\n                backgroundColor: Theme.of(context).scaffoldBackgroundColor,\n                showDragHandle: true,\n                context: context,\n                builder: (context) {\n                  return SourceSheet(tabController: sourceTabController, infoController: infoController);\n                },\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/info_tabview.dart",
    "content": "import 'dart:ui' as ui;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'package:kazumi/bean/card/comments_card.dart';\nimport 'package:kazumi/bean/card/character_card.dart';\nimport 'package:kazumi/bean/card/staff_card.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:skeletonizer/skeletonizer.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/modules/characters/character_item.dart';\nimport 'package:kazumi/modules/staff/staff_item.dart';\n\nclass InfoTabView extends StatefulWidget {\n  const InfoTabView({\n    super.key,\n    required this.commentsQueryTimeout,\n    required this.charactersQueryTimeout,\n    required this.staffQueryTimeout,\n    required this.tabController,\n    required this.loadMoreComments,\n    required this.loadCharacters,\n    required this.loadStaff,\n    required this.bangumiItem,\n    required this.commentsList,\n    required this.characterList,\n    required this.staffList,\n    required this.isLoading,\n  });\n\n  final bool commentsQueryTimeout;\n  final bool charactersQueryTimeout;\n  final bool staffQueryTimeout;\n  final TabController tabController;\n  final Future<void> Function({int offset}) loadMoreComments;\n  final Future<void> Function() loadCharacters;\n  final Future<void> Function() loadStaff;\n  final BangumiItem bangumiItem;\n  final List<CommentItem> commentsList;\n  final List<CharacterItem> characterList;\n  final List<StaffFullItem> staffList;\n  final bool isLoading;\n\n  @override\n  State<InfoTabView> createState() => _InfoTabViewState();\n}\n\nclass _InfoTabViewState extends State<InfoTabView>\n    with SingleTickerProviderStateMixin {\n  final maxWidth = 950.0;\n  bool fullIntro = false;\n  bool fullTag = false;\n\n  Widget get infoBody {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: SizedBox(\n          width: MediaQuery.sizeOf(context).width > maxWidth\n              ? maxWidth\n              : MediaQuery.sizeOf(context).width - 32,\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Text('简介', style: TextStyle(fontSize: 18)),\n              const SizedBox(height: 8),\n              // https://stackoverflow.com/questions/54091055/flutter-how-to-get-the-number-of-text-lines\n              // only show expand button when line > 7\n              LayoutBuilder(builder: (context, constraints) {\n                final span = TextSpan(text: widget.bangumiItem.summary);\n                final tp =\n                    TextPainter(text: span, textDirection: TextDirection.ltr);\n                tp.layout(maxWidth: constraints.maxWidth);\n                final numLines = tp.computeLineMetrics().length;\n                if (numLines > 7) {\n                  return Column(\n                    crossAxisAlignment: CrossAxisAlignment.end,\n                    children: [\n                      SizedBox(\n                        // make intro expandable\n                        height: fullIntro ? null : 120,\n                        width: MediaQuery.sizeOf(context).width > maxWidth\n                            ? maxWidth\n                            : MediaQuery.sizeOf(context).width - 32,\n                        child: SelectableText(\n                          widget.bangumiItem.summary,\n                          textAlign: TextAlign.start,\n                          scrollBehavior: const ScrollBehavior().copyWith(\n                            scrollbars: false,\n                          ),\n                          scrollPhysics: NeverScrollableScrollPhysics(),\n                          selectionHeightStyle: ui.BoxHeightStyle.max,\n                        ),\n                      ),\n                      TextButton(\n                        onPressed: () {\n                          setState(() {\n                            fullIntro = !fullIntro;\n                          });\n                        },\n                        child: Text(fullIntro ? '加载更少' : '加载更多'),\n                      ),\n                    ],\n                  );\n                } else {\n                  return SelectableText(\n                    widget.bangumiItem.summary,\n                    textAlign: TextAlign.start,\n                    scrollPhysics: NeverScrollableScrollPhysics(),\n                    selectionHeightStyle: ui.BoxHeightStyle.max,\n                  );\n                }\n              }),\n              const SizedBox(height: 16),\n              Text('标签', style: TextStyle(fontSize: 18)),\n              const SizedBox(height: 8),\n              Wrap(\n                spacing: 8.0,\n                runSpacing: Utils.isDesktop() ? 8 : 0,\n                children: List<Widget>.generate(\n                    fullTag || widget.bangumiItem.tags.length < 13\n                        ? widget.bangumiItem.tags.length\n                        : 13, (int index) {\n                  if (!fullTag && index == 12) {\n                    // make tag expandable\n                    return ActionChip(\n                      label: Text(\n                        '更多 +',\n                        style: TextStyle(\n                            color: Theme.of(context).colorScheme.primary),\n                      ),\n                      onPressed: () {\n                        setState(() {\n                          fullTag = !fullTag;\n                        });\n                      },\n                    );\n                  }\n                  return ActionChip(\n                    label: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        Text('${widget.bangumiItem.tags[index].name} '),\n                        Text(\n                          '${widget.bangumiItem.tags[index].count}',\n                          style: TextStyle(\n                              color: Theme.of(context).colorScheme.primary),\n                        ),\n                      ],\n                    ),\n                    onPressed: () {\n                      Modular.to.pushNamed(\n                          '/search/${widget.bangumiItem.tags[index].name}');\n                    },\n                  );\n                }).toList(),\n              )\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  /// Bone for Skeleton Loader\n  Widget get infoBodyBone {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: SizedBox(\n          width: MediaQuery.sizeOf(context).width > maxWidth\n              ? maxWidth\n              : MediaQuery.sizeOf(context).width - 32,\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Skeletonizer.zone(child: Bone.text(fontSize: 18, width: 50)),\n              const SizedBox(height: 8),\n              Skeletonizer.zone(child: Bone.multiText(lines: 7)),\n              const SizedBox(height: 16),\n              Skeletonizer.zone(child: Bone.text(fontSize: 18, width: 50)),\n              const SizedBox(height: 8),\n              if (widget.isLoading)\n                Skeletonizer.zone(\n                  child: Wrap(\n                    spacing: 8.0,\n                    runSpacing: 8.0,\n                    children: List.generate(\n                        4, (_) => Bone.button(uniRadius: 8, height: 32)),\n                  ),\n                ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget get commentsListBody {\n    return Builder(\n      builder: (BuildContext context) {\n        return NotificationListener<ScrollEndNotification>(\n          onNotification: (scrollEnd) {\n            final metrics = scrollEnd.metrics;\n            if (metrics.pixels >= metrics.maxScrollExtent - 200) {\n              widget.loadMoreComments(offset: widget.commentsList.length);\n            }\n            return true;\n          },\n          child: CustomScrollView(\n            scrollBehavior: const ScrollBehavior().copyWith(\n              scrollbars: false,\n            ),\n            key: PageStorageKey<String>('吐槽'),\n            slivers: <Widget>[\n              SliverOverlapInjector(\n                handle:\n                    NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n              ),\n              SliverLayoutBuilder(builder: (context, _) {\n                if (widget.commentsList.isNotEmpty) {\n                  return SliverList.separated(\n                    addAutomaticKeepAlives: false,\n                    itemCount: widget.commentsList.length,\n                    itemBuilder: (context, index) {\n                      return SafeArea(\n                        top: false,\n                        bottom: false,\n                        child: Center(\n                          child: Padding(\n                            padding:\n                                const EdgeInsets.symmetric(horizontal: 16.0),\n                            child: SizedBox(\n                              width: MediaQuery.sizeOf(context).width > maxWidth\n                                  ? maxWidth\n                                  : MediaQuery.sizeOf(context).width - 32,\n                              child: CommentsCard(\n                                commentItem: widget.commentsList[index],\n                              ),\n                            ),\n                          ),\n                        ),\n                      );\n                    },\n                    separatorBuilder: (BuildContext context, int index) {\n                      return SafeArea(\n                        top: false,\n                        bottom: false,\n                        child: Center(\n                          child: Padding(\n                            padding:\n                                const EdgeInsets.symmetric(horizontal: 16.0),\n                            child: SizedBox(\n                              width: MediaQuery.sizeOf(context).width > maxWidth\n                                  ? maxWidth\n                                  : MediaQuery.sizeOf(context).width - 32,\n                              child: Divider(\n                                  thickness: 0.5, indent: 10, endIndent: 10),\n                            ),\n                          ),\n                        ),\n                      );\n                    },\n                  );\n                }\n                if (widget.commentsQueryTimeout) {\n                  return SliverFillRemaining(\n                    child: GeneralErrorWidget(\n                      errMsg: '获取失败，请重试',\n                      actions: [\n                        GeneralErrorButton(\n                          onPressed: () {\n                            widget.loadMoreComments(\n                                offset: widget.commentsList.length);\n                          },\n                          text: '重试',\n                        ),\n                      ],\n                    ),\n                  );\n                }\n                return SliverList.builder(\n                  itemCount: 4,\n                  itemBuilder: (context, _) {\n                    return SafeArea(\n                      top: false,\n                      bottom: false,\n                      child: Center(\n                        child: Padding(\n                          padding: const EdgeInsets.all(16),\n                          child: SizedBox(\n                            width: MediaQuery.sizeOf(context).width > maxWidth\n                                ? maxWidth\n                                : MediaQuery.sizeOf(context).width - 32,\n                            child: CommentsCard.bone(),\n                          ),\n                        ),\n                      ),\n                    );\n                  },\n                );\n              })\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget get staffListBody {\n    return Builder(\n      builder: (BuildContext context) {\n        return CustomScrollView(\n          scrollBehavior: const ScrollBehavior().copyWith(\n            scrollbars: false,\n          ),\n          key: PageStorageKey<String>('制作人员'),\n          slivers: <Widget>[\n            SliverOverlapInjector(\n              handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n            ),\n            SliverLayoutBuilder(builder: (context, _) {\n              if (widget.staffList.isNotEmpty) {\n                return SliverList.builder(\n                  itemCount: widget.staffList.length,\n                  itemBuilder: (context, index) {\n                    return Center(\n                      child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 16.0),\n                        child: SizedBox(\n                          width: MediaQuery.sizeOf(context).width > maxWidth\n                              ? maxWidth\n                              : MediaQuery.sizeOf(context).width - 32,\n                          child: StaffCard(\n                            staffFullItem: widget.staffList[index],\n                          ),\n                        ),\n                      ),\n                    );\n                  },\n                );\n              }\n              if (widget.staffQueryTimeout) {\n                return SliverFillRemaining(\n                  child: GeneralErrorWidget(\n                    errMsg: '获取失败，请重试',\n                    actions: [\n                      GeneralErrorButton(\n                        onPressed: () {\n                          widget.loadStaff();\n                        },\n                        text: '重试',\n                      ),\n                    ],\n                  ),\n                );\n              }\n              return SliverList.builder(\n                itemCount: 8,\n                itemBuilder: (context, _) {\n                  return Align(\n                    alignment: Alignment.topCenter,\n                    child: SizedBox(\n                      width: MediaQuery.sizeOf(context).width > maxWidth\n                          ? maxWidth\n                          : MediaQuery.sizeOf(context).width - 32,\n                      child: Skeletonizer.zone(\n                        child: ListTile(\n                          leading: Bone.circle(size: 36),\n                          title: Bone.text(width: 100),\n                          subtitle: Bone.text(width: 80),\n                        ),\n                      ),\n                    ),\n                  );\n                },\n              );\n            }),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget get charactersListBody {\n    return Builder(\n      builder: (BuildContext context) {\n        return CustomScrollView(\n          scrollBehavior: const ScrollBehavior().copyWith(\n            scrollbars: false,\n          ),\n          key: PageStorageKey<String>('角色'),\n          slivers: <Widget>[\n            SliverOverlapInjector(\n              handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n            ),\n            SliverLayoutBuilder(builder: (context, _) {\n              if (widget.characterList.isNotEmpty) {\n                return SliverList.builder(\n                  itemCount: widget.characterList.length,\n                  itemBuilder: (context, index) {\n                    return Center(\n                      child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 16.0),\n                        child: SizedBox(\n                          width: MediaQuery.sizeOf(context).width > maxWidth\n                              ? maxWidth\n                              : MediaQuery.sizeOf(context).width - 32,\n                          child: CharacterCard(\n                            characterItem: widget.characterList[index],\n                          ),\n                        ),\n                      ),\n                    );\n                  },\n                );\n              }\n              if (widget.charactersQueryTimeout) {\n                return SliverFillRemaining(\n                  child: GeneralErrorWidget(\n                    errMsg: '获取失败，请重试',\n                    actions: [\n                      GeneralErrorButton(\n                        onPressed: () {\n                          widget.loadCharacters();\n                        },\n                        text: '重试',\n                      ),\n                    ],\n                  ),\n                );\n              }\n              return SliverList.builder(\n                itemCount: 4,\n                itemBuilder: (context, _) {\n                  return Align(\n                    alignment: Alignment.topCenter,\n                    child: SizedBox(\n                      width: MediaQuery.sizeOf(context).width > maxWidth\n                          ? maxWidth\n                          : MediaQuery.sizeOf(context).width - 32,\n                      child: Skeletonizer.zone(\n                        child: ListTile(\n                          leading: Bone.circle(size: 36),\n                          title: Bone.text(width: 100),\n                          subtitle: Bone.text(width: 80),\n                        ),\n                      ),\n                    ),\n                  );\n                },\n              );\n            }),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TabBarView(\n      controller: widget.tabController,\n      children: [\n        Builder(\n          // This Builder is needed to provide a BuildContext that is\n          // \"inside\" the NestedScrollView, so that\n          // sliverOverlapAbsorberHandleFor() can find the\n          // NestedScrollView.\n          builder: (BuildContext context) {\n            return CustomScrollView(\n              scrollBehavior: const ScrollBehavior().copyWith(\n                scrollbars: false,\n              ),\n              // The PageStorageKey should be unique to this ScrollView;\n              // it allows the list to remember its scroll position when\n              // the tab view is not on the screen.\n              key: PageStorageKey<String>('概览'),\n              slivers: <Widget>[\n                SliverOverlapInjector(\n                  handle:\n                      NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n                ),\n                SliverToBoxAdapter(\n                  child: SafeArea(\n                    top: false,\n                    bottom: false,\n                    child: widget.isLoading ? infoBodyBone : infoBody,\n                  ),\n                ),\n              ],\n            );\n          },\n        ),\n        commentsListBody,\n        charactersListBody,\n        Builder(\n          builder: (BuildContext context) {\n            return CustomScrollView(\n              scrollBehavior: const ScrollBehavior().copyWith(\n                scrollbars: false,\n              ),\n              key: PageStorageKey<String>('评论'),\n              slivers: <Widget>[\n                SliverOverlapInjector(\n                  handle:\n                      NestedScrollView.sliverOverlapAbsorberHandleFor(context),\n                ),\n                // TODO: 评论区\n                SliverFillRemaining(\n                  child: Center(child: Text('施工中')),\n                ),\n              ],\n            );\n          },\n        ),\n        staffListBody,\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/info/source_sheet.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/info/info_controller.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'package:kazumi/request/query_manager.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'dart:async';\nimport 'dart:convert';\nimport 'package:kazumi/providers/captcha/captcha_provider.dart';\nimport 'package:kazumi/plugins/anti_crawler_config.dart';\n\nclass SourceSheet extends StatefulWidget {\n  const SourceSheet({\n    super.key,\n    required this.tabController,\n    required this.infoController,\n  });\n\n  final TabController tabController;\n  final InfoController infoController;\n\n  @override\n  State<SourceSheet> createState() => _SourceSheetState();\n}\n\nclass _SourceSheetState extends State<SourceSheet>\n    with SingleTickerProviderStateMixin {\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final CollectController collectController = Modular.get<CollectController>();\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  late String keyword;\n\n  /// Concurrent query manager\n  QueryManager? queryManager;\n\n  /// Captcha solving provider (created on demand)\n  CaptchaProvider? _captchaProvider;\n\n  /// Timeout timer waiting for captcha verification result\n  Timer? _captchaVerifyTimer;\n\n  @override\n  void initState() {\n    keyword = widget.infoController.bangumiItem.nameCn == ''\n        ? widget.infoController.bangumiItem.name\n        : widget.infoController.bangumiItem.nameCn;\n    queryManager = QueryManager(infoController: widget.infoController);\n    queryManager?.queryAllSource(keyword);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    queryManager?.cancel();\n    queryManager = null;\n    _captchaProvider?.dispose();\n    _captchaProvider = null;\n    _captchaVerifyTimer?.cancel();\n    _captchaVerifyTimer = null;\n    super.dispose();\n  }\n\n  /// 根据插件的验证类型分发到对应的验证对话框\n  void showAntiCrawlerDialog(Plugin plugin) {\n    switch (plugin.antiCrawlerConfig.captchaType) {\n      case CaptchaType.autoClickButton:\n        showButtonClickDialog(plugin);\n        break;\n      default:\n        showCaptchaDialog(plugin);\n    }\n  }\n\n  void showCaptchaDialog(Plugin plugin) {\n    final captchaImageNotifier = ValueNotifier<String?>(null);\n    final submittingNotifier = ValueNotifier<bool>(false);\n    final TextEditingController codeController = TextEditingController();\n\n    /// flag whether verification has passed, used to distinguish normal dismissal from cancellation in onDismiss\n    bool verified = false;\n\n    _captchaProvider?.dispose();\n    _captchaProvider = CaptchaProvider();\n\n    final searchUrl = plugin.searchURL.replaceAll('@keyword', keyword);\n\n    _captchaProvider!.loadForCaptcha(\n      searchUrl,\n      plugin.antiCrawlerConfig.captchaImage,\n      inputXpath: plugin.antiCrawlerConfig.captchaInput,\n    );\n\n    final imageSub = _captchaProvider!.onCaptchaImageUrl.listen((url) {\n      if (url != null) captchaImageNotifier.value = url;\n    });\n\n    Future<void> doSubmit() async {\n      if (submittingNotifier.value) return;\n      if (codeController.text.trim().isEmpty) {\n        KazumiDialog.showToast(message: '请输入验证码');\n        return;\n      }\n      submittingNotifier.value = true;\n      await _captchaProvider?.submitCaptcha(\n        captchaCode: codeController.text.trim(),\n        inputXpath: plugin.antiCrawlerConfig.captchaInput,\n        buttonXpath: plugin.antiCrawlerConfig.captchaButton,\n        pluginName: plugin.name,\n        onVerified: () {\n          _captchaVerifyTimer?.cancel();\n          _captchaVerifyTimer = null;\n          verified = true;\n          KazumiDialog.dismiss();\n          // show a 3s countdown progress dialog before re-querying,\n          // to avoid triggering rate limits immediately after verification.\n          KazumiDialog.showTimedSuccessDialog(\n            title: '验证成功',\n            message: '正在重新检索，请稍候…',\n            onComplete: () => queryManager?.querySource(keyword, plugin.name),\n          );\n        },\n      );\n      // submitCaptcha completes after the JS button click is fired.\n      // Start the 8-second timeout only NOW, waiting for the webview to\n      // detect the captcha disappearing and call onVerified.\n      if (!verified) {\n        _captchaVerifyTimer?.cancel();\n        _captchaVerifyTimer = Timer(const Duration(seconds: 8), () {\n          if (!verified) {\n            KazumiDialog.dismiss();\n          }\n        });\n      }\n    }\n\n    KazumiDialog.show(\n      onDismiss: () async {\n        _captchaVerifyTimer?.cancel();\n        _captchaVerifyTimer = null;\n        // Cancel the image subscription before disposing the notifier to\n        // prevent late stream events writing to an already-disposed notifier.\n        imageSub.cancel();\n        codeController.dispose();\n        captchaImageNotifier.dispose();\n        submittingNotifier.dispose();\n        // Capture the current provider instance locally NOW, before any await.\n        // Without this, an async gap could allow _captchaProvider to be\n        // replaced (or nulled by _SourceSheetState.dispose()), causing the\n        // closure to dispose the wrong/already-disposed instance.\n        final provider = _captchaProvider;\n        _captchaProvider = null;\n        if (!verified) {\n          await provider?.saveAndUnload(plugin.name);\n          provider?.dispose();\n          queryManager?.querySource(keyword, plugin.name);\n        } else {\n          provider?.dispose();\n        }\n      },\n      builder: (context) {\n        return Dialog(\n          clipBehavior: Clip.antiAlias,\n          child: Padding(\n            padding: const EdgeInsets.all(24),\n            child: SizedBox(\n              width: 400,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.center,\n                children: [\n                  Text(\n                    '验证码验证',\n                    style: Theme.of(context).textTheme.titleLarge,\n                  ),\n                  const SizedBox(height: 4),\n                  Text(\n                    '${plugin.name} 需要验证码验证',\n                    style: Theme.of(context).textTheme.bodySmall,\n                  ),\n                  const SizedBox(height: 20),\n                  ValueListenableBuilder<String?>(\n                    valueListenable: captchaImageNotifier,\n                    builder: (context, imageUrl, _) {\n                      if (imageUrl == null) {\n                        return const Column(\n                          children: [\n                            CircularProgressIndicator(),\n                            SizedBox(height: 12),\n                            Text('正在加载验证码图片...'),\n                          ],\n                        );\n                      }\n                      return ValueListenableBuilder<bool>(\n                        valueListenable: submittingNotifier,\n                        builder: (context, isSubmitting, _) {\n                          return Column(\n                            children: [\n                              ClipRRect(\n                                borderRadius: BorderRadius.circular(8),\n                                child: Image.memory(\n                                  base64Decode(imageUrl.split(',').last),\n                                  height: 80,\n                                  fit: BoxFit.contain,\n                                  errorBuilder: (context, error, _) =>\n                                      const Text('图片解码失败'),\n                                ),\n                              ),\n                              const SizedBox(height: 16),\n                              TextField(\n                                controller: codeController,\n                                autofocus: true,\n                                enabled: !isSubmitting,\n                                decoration: const InputDecoration(\n                                  labelText: '请输入验证码',\n                                  border: OutlineInputBorder(),\n                                ),\n                                onSubmitted:\n                                    isSubmitting ? null : (_) => doSubmit(),\n                              ),\n                            ],\n                          );\n                        },\n                      );\n                    },\n                  ),\n                  const SizedBox(height: 20),\n                  ListenableBuilder(\n                    listenable: Listenable.merge(\n                        [captchaImageNotifier, submittingNotifier]),\n                    builder: (context, _) {\n                      final isImageLoading = captchaImageNotifier.value == null;\n                      final isSubmitting = submittingNotifier.value;\n                      final isDisabled = isImageLoading || isSubmitting;\n                      return Row(\n                        mainAxisAlignment: MainAxisAlignment.end,\n                        children: [\n                          TextButton(\n                            onPressed: () => KazumiDialog.dismiss(),\n                            child: Text(\n                              '取消',\n                              style: TextStyle(\n                                  color: Theme.of(context).colorScheme.outline),\n                            ),\n                          ),\n                          const SizedBox(width: 8),\n                          FilledButton(\n                            onPressed: isDisabled ? null : doSubmit,\n                            child: isSubmitting\n                                ? const SizedBox(\n                                    width: 18,\n                                    height: 18,\n                                    child: CircularProgressIndicator(\n                                      strokeWidth: 2,\n                                    ),\n                                  )\n                                : const Text('提交'),\n                          ),\n                        ],\n                      );\n                    },\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void showButtonClickDialog(Plugin plugin) {\n    /// flag whether onVerified was fired by the auto-click flow (cookies already saved + page unloaded)\n    bool autoVerified = false;\n\n    _captchaProvider?.dispose();\n    _captchaProvider = CaptchaProvider();\n\n    final searchUrl = plugin.searchURL.replaceAll('@keyword', keyword);\n\n    void onVerified() {\n      if (autoVerified) return;\n      autoVerified = true;\n      KazumiDialog.dismiss();\n      // show a 3s countdown progress dialog before re-querying\n      KazumiDialog.showTimedSuccessDialog(\n        title: '验证成功',\n        message: '正在重新检索，请稍候…',\n        onComplete: () => queryManager?.querySource(keyword, plugin.name),\n      );\n    }\n\n    _captchaProvider!.loadForButtonClick(\n      url: searchUrl,\n      buttonXpath: plugin.antiCrawlerConfig.captchaButton,\n      pluginName: plugin.name,\n      onVerified: onVerified,\n    );\n\n    KazumiDialog.show(\n      onDismiss: () async {\n        // Capture the current provider instance locally before any await.\n        final provider = _captchaProvider;\n        _captchaProvider = null;\n        if (autoVerified) {\n          // auto-verify already saved cookies and unloaded the page\n          provider?.dispose();\n        } else {\n          // save whatever cookies are present and unload the page\n          await provider?.saveAndUnload(plugin.name);\n          provider?.dispose();\n          queryManager?.querySource(keyword, plugin.name);\n        }\n      },\n      builder: (context) => Dialog(\n        clipBehavior: Clip.antiAlias,\n        child: Padding(\n          padding: const EdgeInsets.all(24),\n          child: SizedBox(\n            width: 400,\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.center,\n              children: [\n                Text(\n                  '自动验证中',\n                  style: Theme.of(context).textTheme.titleLarge,\n                ),\n                const SizedBox(height: 4),\n                Text(\n                  '${plugin.name} 正在自动完成验证，请稍候',\n                  style: Theme.of(context).textTheme.bodySmall,\n                ),\n                const SizedBox(height: 24),\n                const CircularProgressIndicator(),\n                const SizedBox(height: 12),\n                Text(\n                  '已检测到验证按钮并模拟点击，等待验证通过…',\n                  style: Theme.of(context).textTheme.bodyMedium,\n                  textAlign: TextAlign.center,\n                ),\n                const SizedBox(height: 20),\n                Align(\n                  alignment: Alignment.centerRight,\n                  child: TextButton(\n                    onPressed: () => KazumiDialog.dismiss(),\n                    child: Text(\n                      '取消',\n                      style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildPluginView(Plugin plugin, List<Widget> cardList) {\n    final status =\n        widget.infoController.pluginSearchStatus[plugin.name];\n    if (status == 'pending') {\n      return const Center(child: CircularProgressIndicator());\n    }\n    if (status == 'captcha') {\n      return GeneralErrorWidget(\n        errMsg: '${plugin.name} 需要验证码验证',\n        actions: [\n          GeneralErrorButton(\n            onPressed: () => showAntiCrawlerDialog(plugin),\n            text: '进行验证',\n          ),\n          GeneralErrorButton(\n            onPressed: () => queryManager?.querySource(keyword, plugin.name),\n            text: '重试',\n          ),\n        ],\n      );\n    }\n    if (status == 'noResult') {\n      return GeneralErrorWidget(\n        errMsg: '${plugin.name} 无结果 使用别名或左右滑动以切换到其他视频来源',\n        actions: [\n          GeneralErrorButton(\n            onPressed: () => showAliasSearchDialog(plugin.name),\n            text: '别名检索',\n          ),\n          GeneralErrorButton(\n            onPressed: () => showCustomSearchDialog(plugin.name),\n            text: '手动检索',\n          ),\n        ],\n      );\n    }\n    if (status == 'error') {\n      return GeneralErrorWidget(\n        errMsg: '${plugin.name} 检索失败 重试或左右滑动以切换到其他视频来源',\n        actions: [\n          GeneralErrorButton(\n            onPressed: () => queryManager?.querySource(keyword, plugin.name),\n            text: '重试',\n          ),\n        ],\n      );\n    }\n    return ListView(children: cardList);\n  }\n\n  void showAliasSearchDialog(String pluginName) {\n    if (widget.infoController.bangumiItem.alias.isEmpty) {\n      KazumiDialog.showToast(message: '无可用别名，试试手动检索');\n      return;\n    }\n    final aliasNotifier =\n        ValueNotifier<List<String>>(widget.infoController.bangumiItem.alias);\n    KazumiDialog.show(builder: (context) {\n      return Dialog(\n        clipBehavior: Clip.antiAlias,\n        child: SizedBox(\n          width: 560,\n          child: ValueListenableBuilder<List<String>>(\n            valueListenable: aliasNotifier,\n            builder: (context, aliasList, child) {\n              return ListView(\n                shrinkWrap: true,\n                children: aliasList.asMap().entries.map((entry) {\n                  final index = entry.key;\n                  final alias = entry.value;\n                  return ListTile(\n                    title: Text(alias),\n                    trailing: IconButton(\n                      onPressed: () {\n                        KazumiDialog.show(\n                          builder: (context) {\n                            return AlertDialog(\n                              title: const Text('删除确认'),\n                              content: const Text('删除后无法恢复，确认要永久删除这个别名吗？'),\n                              actions: [\n                                TextButton(\n                                  onPressed: () {\n                                    KazumiDialog.dismiss();\n                                  },\n                                  child: Text(\n                                    '取消',\n                                    style: TextStyle(\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .outline),\n                                  ),\n                                ),\n                                TextButton(\n                                  onPressed: () {\n                                    KazumiDialog.dismiss();\n                                    aliasList.removeAt(index);\n                                    aliasNotifier.value = List.from(aliasList);\n                                    collectController.updateLocalCollect(\n                                        widget.infoController.bangumiItem);\n                                    if (aliasList.isEmpty) {\n                                      // pop whole dialog when empty\n                                      Navigator.of(context).pop();\n                                    }\n                                  },\n                                  child: const Text('确认'),\n                                ),\n                              ],\n                            );\n                          },\n                        );\n                      },\n                      icon: Icon(Icons.delete),\n                    ),\n                    onTap: () {\n                      KazumiDialog.dismiss();\n                      queryManager?.querySource(alias, pluginName);\n                    },\n                  );\n                }).toList(),\n              );\n            },\n          ),\n        ),\n      );\n    });\n  }\n\n  void showCustomSearchDialog(String pluginName) {\n    KazumiDialog.show(\n      builder: (context) {\n        final TextEditingController textController = TextEditingController();\n        return AlertDialog(\n          title: const Text('输入别名'),\n          content: TextField(\n            controller: textController,\n            onSubmitted: (keyword) {\n              if (textController.text != '') {\n                widget.infoController.bangumiItem.alias\n                    .add(textController.text);\n                KazumiDialog.dismiss();\n                queryManager?.querySource(textController.text, pluginName);\n              }\n            },\n          ),\n          actions: [\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n              },\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () {\n                if (textController.text != '') {\n                  widget.infoController.bangumiItem.alias\n                      .add(textController.text);\n                  collectController\n                      .updateLocalCollect(widget.infoController.bangumiItem);\n                  KazumiDialog.dismiss();\n                  queryManager?.querySource(textController.text, pluginName);\n                }\n              },\n              child: const Text(\n                '确认',\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DefaultTabController(\n      length: 3,\n      child: Scaffold(\n        body: Column(\n          children: [\n            Row(\n              children: [\n                Expanded(\n                  child: TabBar(\n                    isScrollable: true,\n                    tabAlignment: TabAlignment.center,\n                    dividerHeight: 0,\n                    controller: widget.tabController,\n                    tabs: pluginsController.pluginList\n                        .map(\n                          (plugin) => Observer(\n                            builder: (context) {\n                              return Tab(\n                                child: Row(\n                                  children: [\n                                    Text(\n                                      plugin.name,\n                                      overflow: TextOverflow.ellipsis,\n                                      style: TextStyle(\n                                          fontSize: Theme.of(context)\n                                              .textTheme\n                                              .titleMedium!\n                                              .fontSize,\n                                          color: Theme.of(context)\n                                              .colorScheme\n                                              .onSurface),\n                                    ),\n                                    const SizedBox(width: 5.0),\n                                    Container(\n                                      width: 8.0,\n                                      height: 8.0,\n                                      decoration: BoxDecoration(\n                                        color: switch (widget.infoController\n                                            .pluginSearchStatus[plugin.name]) {\n                                          'success' => Colors.green,\n                                          'noResult' => Colors.orange,\n                                          'captcha' => Colors.blue,\n                                          'error' => Colors.red,\n                                          _ => Colors.grey,\n                                        },\n                                        shape: BoxShape.circle,\n                                      ),\n                                    ),\n                                  ],\n                                ),\n                              );\n                            },\n                          ),\n                        )\n                        .toList(),\n                  ),\n                ),\n                IconButton(\n                  onPressed: () {\n                    int currentIndex = widget.tabController.index;\n                    launchUrl(\n                      Uri.parse(pluginsController\n                          .pluginList[currentIndex].searchURL\n                          .replaceFirst('@keyword', keyword)),\n                      mode: LaunchMode.externalApplication,\n                    );\n                  },\n                  icon: const Icon(Icons.open_in_browser_rounded),\n                ),\n                const SizedBox(width: 4),\n              ],\n            ),\n            const Divider(height: 1),\n            Expanded(\n              child: Observer(\n                builder: (context) => TabBarView(\n                  controller: widget.tabController,\n                  children: List.generate(pluginsController.pluginList.length,\n                      (pluginIndex) {\n                    var plugin = pluginsController.pluginList[pluginIndex];\n                    var cardList = <Widget>[];\n                    for (var searchResponse\n                        in widget.infoController.pluginSearchResponseList) {\n                      if (searchResponse.pluginName == plugin.name) {\n                        for (var searchItem in searchResponse.data) {\n                          cardList.add(\n                            Card(\n                              elevation: 0,\n                              margin: const EdgeInsets.only(\n                                  left: 10, right: 10, top: 10),\n                              child: InkWell(\n                                borderRadius: BorderRadius.circular(12),\n                                onTap: () async {\n                                  KazumiDialog.showLoading(\n                                    msg: '获取中',\n                                    barrierDismissible: Utils.isDesktop(),\n                                    onDismiss: () {\n                                      videoPageController.cancelQueryRoads();\n                                    },\n                                  );\n                                  videoPageController.bangumiItem =\n                                      widget.infoController.bangumiItem;\n                                  videoPageController.currentPlugin = plugin;\n                                  videoPageController.title = searchItem.name;\n                                  videoPageController.src = searchItem.src;\n                                  try {\n                                    await videoPageController.queryRoads(\n                                        searchItem.src, plugin.name);\n                                    KazumiDialog.dismiss();\n                                    Modular.to.pushNamed('/video/');\n                                  } catch (_) {\n                                    KazumiLogger().w(\n                                        \"QueryManager: failed to query video playlist\");\n                                    KazumiDialog.dismiss();\n                                  }\n                                },\n                                child: Padding(\n                                  padding: const EdgeInsets.all(20),\n                                  child: Text(searchItem.name),\n                                ),\n                              ),\n                            ),\n                          );\n                        }\n                      }\n                    }\n                    return buildPluginView(plugin, cardList);\n                  }),\n                ),\n              ),\n            )\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/init_page.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/my/my_controller.dart';\nimport 'package:kazumi/utils/webdav.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:flutter/services.dart' show rootBundle;\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:provider/provider.dart';\nimport 'package:kazumi/bean/settings/theme_provider.dart';\nimport 'package:kazumi/shaders/shaders_controller.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\nimport 'package:kazumi/utils/background_download_service.dart';\n\nclass InitPage extends StatefulWidget {\n  const InitPage({super.key});\n\n  @override\n  State<InitPage> createState() => _InitPageState();\n}\n\nclass _InitPageState extends State<InitPage> {\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  final CollectController collectController = Modular.get<CollectController>();\n  final ShadersController shadersController = Modular.get<ShadersController>();\n  final MyController myController = Modular.get<MyController>();\n  final DownloadController downloadController =\n      Modular.get<DownloadController>();\n  Box setting = GStorage.setting;\n  late final ThemeProvider themeProvider;\n\n  @override\n  void initState() {\n    super.initState();\n    themeProvider = Provider.of<ThemeProvider>(context, listen: false);\n    _initializeApp();\n  }\n\n  Future<void> _initializeApp() async {\n    _migrateStorage();\n    _loadShaders();\n    _loadDanmakuShield();\n    _webDavInit();\n    try {\n      await downloadController.init();\n      _setupBackgroundDownloadNavigation();\n    } catch (e) {\n      KazumiLogger().e('InitPage: downloadController.init() failed', error: e);\n    }\n\n    await _checkRunningOnX11();\n    await _pluginInit();\n\n    _startDefaultPage();\n    // delay to ensure that the default page is fully loaded\n    await Future.delayed(const Duration(milliseconds: 500));\n    _update();\n  }\n\n  void _setupBackgroundDownloadNavigation() {\n    final backgroundService = BackgroundDownloadService();\n\n    backgroundService.onNavigateToDownloadRequested = () {\n      Future.delayed(const Duration(milliseconds: 300), () {\n        try {\n          if (Modular.to.path.contains('/download')) return;\n          Modular.to.pushNamed('/settings/download/');\n        } catch (e) {\n          KazumiLogger()\n              .w('InitPage: failed to navigate to download page', error: e);\n        }\n      });\n    };\n\n    backgroundService.onNotificationPermissionRequired = () async {\n      final result = await KazumiDialog.show<bool>(\n        clickMaskDismiss: false,\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('需要通知权限'),\n            content: const Text(\n              '开启通知权限后，可以在后台下载时显示进度，并防止系统终止下载任务。\\n\\n'\n              '如果拒绝，下载功能仍可使用，但在后台时可能被系统中断。',\n            ),\n            actions: [\n              TextButton(\n                onPressed: () => KazumiDialog.dismiss(popWith: false),\n                child: Text(\n                  '稍后再说',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline),\n                ),\n              ),\n              TextButton(\n                onPressed: () => KazumiDialog.dismiss(popWith: true),\n                child: const Text('允许'),\n              ),\n            ],\n          );\n        },\n      );\n      return result ?? false;\n    };\n  }\n\n  void _startDefaultPage() {\n    final defaultStartupPage = setting.get(\n      SettingBoxKey.defaultStartupPage,\n      defaultValue: '/tab/popular/',\n    );\n    // Workaround for dynamic_color. dynamic_color need PlatformChannel to get color, it takes time.\n    // setDynamic here to avoid white screen flash when themeMode is dark.\n    themeProvider.setDynamic(\n        setting.get(SettingBoxKey.useDynamicColor, defaultValue: false));\n    Modular.to.navigate(defaultStartupPage);\n  }\n\n  // migrate collect from old version (favorites)\n  Future<void> _migrateStorage() async {\n    await collectController.migrateCollect();\n  }\n\n  Future<void> _loadShaders() async {\n    await shadersController.copyShadersToExternalDirectory();\n  }\n\n  Future<void> _loadDanmakuShield() async {\n    myController.loadShieldList();\n  }\n\n  Future<void> _webDavInit() async {\n    bool webDavEnable =\n        await setting.get(SettingBoxKey.webDavEnable, defaultValue: false);\n    if (webDavEnable) {\n      var webDav = WebDav();\n      KazumiLogger().i('WebDav: Starting WebDav initialization');\n      try {\n        await webDav.init();\n        try {\n          await webDav.downloadAndPatchHistory();\n          KazumiLogger().i('WebDav: Completed syncing watch history');\n        } catch (e) {\n          KazumiDialog.showToast(message: \"同步观看记录失败 ${e.toString()}\");\n        }\n      } catch (e) {\n        KazumiDialog.showToast(message: \"初始化WebDav失败 ${e.toString()}\");\n      }\n    }\n  }\n\n  Future<void> _checkRunningOnX11() async {\n    if (!Platform.isLinux) {\n      return;\n    }\n    bool isRunningOnX11 = await Utils.isRunningOnX11();\n    if (isRunningOnX11) {\n      await KazumiDialog.show(\n        clickMaskDismiss: false,\n        builder: (context) {\n          return PopScope(\n            canPop: false,\n            child: AlertDialog(\n              title: const Text('X11环境检测'),\n              content: const Text(\n                  '检测到您当前运行在X11环境下，Kazumi在X11环境下可能出现性能问题或界面异常，建议切换到Wayland以获得更好的体验。您是否希望在X11下继续使用Kazumi？'),\n              actions: [\n                TextButton(\n                  onPressed: () {\n                    exit(0);\n                  },\n                  child: Text(\n                    '退出',\n                    style:\n                        TextStyle(color: Theme.of(context).colorScheme.outline),\n                  ),\n                ),\n                TextButton(\n                  onPressed: () {\n                    KazumiDialog.dismiss();\n                  },\n                  child: const Text('继续'),\n                ),\n              ],\n            ),\n          );\n        },\n      );\n    }\n  }\n\n  Future<void> _pluginInit() async {\n    String statementsText = '';\n    try {\n      await pluginsController.init();\n      statementsText =\n          await rootBundle.loadString(\"assets/statements/statements.txt\");\n      _pluginUpdate();\n    } catch (_) {}\n    if (pluginsController.pluginList.isEmpty) {\n      await KazumiDialog.show(\n        clickMaskDismiss: false,\n        builder: (context) {\n          return PopScope(\n            canPop: false,\n            child: AlertDialog(\n              title: const Text('免责声明'),\n              scrollable: true,\n              content: Text(statementsText),\n              actions: [\n                TextButton(\n                  onPressed: () {\n                    exit(0);\n                  },\n                  child: Text(\n                    '退出',\n                    style:\n                        TextStyle(color: Theme.of(context).colorScheme.outline),\n                  ),\n                ),\n                TextButton(\n                  onPressed: () async {\n                    try {\n                      await pluginsController.copyPluginsToExternalDirectory();\n                    } catch (_) {}\n                    KazumiDialog.dismiss();\n                    if (!Platform.isAndroid) {\n                      return;\n                    }\n                    await _switchUpdateMirror();\n                  },\n                  child: const Text('已阅读并同意'),\n                ),\n              ],\n            ),\n          );\n        },\n      );\n    }\n  }\n\n  // The function is not completed yet\n  // We simply disable update when the user is using F-Droid mirror\n  // We are trying to meet F-Droid requirement to submit the app\n  // After the app is submitted, we will complete the function\n  Future<void> _switchUpdateMirror() async {\n    await KazumiDialog.show(\n      clickMaskDismiss: false,\n      builder: (context) {\n        return PopScope(\n          canPop: false,\n          child: AlertDialog(\n            title: const Text('更新镜像'),\n            content: const Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Padding(\n                  padding: EdgeInsets.symmetric(vertical: 8),\n                  child: Text(\n                    '您希望从哪里获取应用更新？',\n                    textAlign: TextAlign.left,\n                  ),\n                ),\n                Padding(\n                  padding: EdgeInsets.symmetric(vertical: 8),\n                  child: Text(\n                    'Github镜像为大多数情况下的最佳选择。如果您使用F-Droid应用商店, 请选择F-Droid镜像。',\n                    textAlign: TextAlign.left,\n                  ),\n                ),\n              ],\n            ),\n            actions: [\n              TextButton(\n                onPressed: () {\n                  setting.put(SettingBoxKey.autoUpdate, true);\n                  KazumiDialog.dismiss();\n                },\n                child: const Text(\n                  'Github',\n                ),\n              ),\n              TextButton(\n                onPressed: () {\n                  setting.put(SettingBoxKey.autoUpdate, false);\n                  KazumiDialog.dismiss();\n                },\n                child: Text(\n                  'F-Droid',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _update() async {\n    bool autoUpdate = await setting.get(SettingBoxKey.autoUpdate, defaultValue: true);\n    if (autoUpdate) {\n      Modular.get<MyController>().checkUpdate(type: 'auto');\n    }\n  }\n\n  Future<void> _pluginUpdate() async {\n    await pluginsController.queryPluginHTTPList();\n    int count = 0;\n    for (var plugin in pluginsController.pluginList) {\n      if (pluginsController.pluginUpdateStatus(plugin) == 'updatable') {\n        count++;\n      }\n    }\n    if (count != 0) {\n      KazumiDialog.showToast(message: '检测到 $count 条规则可以更新');\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return const LoadingWidget();\n  }\n}\n\nclass LoadingWidget extends StatelessWidget {\n  const LoadingWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(body: Container());\n  }\n}\n"
  },
  {
    "path": "lib/pages/logs/logs_page.dart",
    "content": "import 'dart:io';\nimport 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\n\nclass LogsPage extends StatefulWidget {\n  const LogsPage({super.key});\n\n  @override\n  State<LogsPage> createState() => _LogsPageState();\n}\n\nclass _LogsPageState extends State<LogsPage> {\n  final List<String> _logLines = [];\n  final ScrollController _scrollController = ScrollController();\n  \n  bool _isLoading = true;\n  bool _hasError = false;\n  String _fullContent = '';\n  \n  static const int _initialLoadCount = 50;\n  static const int _loadMoreCount = 100;\n  int _displayedLines = 0;\n  List<String> _allLines = [];\n\n  @override\n  void initState() {\n    super.initState();\n    _loadLogs();\n    _scrollController.addListener(_onScroll);\n  }\n\n  @override\n  void dispose() {\n    _scrollController.removeListener(_onScroll);\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  void _onScroll() {\n    if (!mounted || _displayedLines >= _allLines.length) {\n      return;\n    }\n    \n    final maxScroll = _scrollController.position.maxScrollExtent;\n    final currentScroll = _scrollController.position.pixels;\n    final threshold = maxScroll * 0.8;\n    \n    if (currentScroll >= threshold) {\n      _loadMoreLines();\n    }\n  }\n\n  Future<void> _loadLogs() async {\n    if (!mounted) return;\n    \n    try {\n      final file = await _getLogsFile();\n      if (!mounted) return;\n      \n      if (await file.exists()) {\n        final content = await file.readAsString();\n        if (!mounted) return;\n        \n        _allLines = content.split('\\n');\n        _fullContent = content;\n        \n        final initialCount = _allLines.length < _initialLoadCount \n            ? _allLines.length \n            : _initialLoadCount;\n        \n        if (!mounted) return;\n        setState(() {\n          _logLines.clear();\n          _logLines.addAll(_allLines.take(initialCount));\n          _displayedLines = initialCount;\n          _isLoading = false;\n        });\n      } else {\n        if (!mounted) return;\n        setState(() {\n          _isLoading = false;\n        });\n      }\n    } catch (e) {\n      if (!mounted) return;\n      setState(() {\n        _hasError = true;\n        _isLoading = false;\n      });\n    }\n  }\n\n  void _loadMoreLines() {\n    if (_displayedLines >= _allLines.length) {\n      return;\n    }\n    \n    // 使用 Future.microtask 避免在构建过程中调用 setState\n    Future.microtask(() {\n      if (!mounted) return;\n      \n      final remainingLines = _allLines.length - _displayedLines;\n      final linesToLoad = remainingLines < _loadMoreCount \n          ? remainingLines \n          : _loadMoreCount;\n      \n      final newLines = _allLines.skip(_displayedLines).take(linesToLoad);\n      \n      if (!mounted) return;\n      setState(() {\n        _logLines.addAll(newLines);\n        _displayedLines += linesToLoad;\n      });\n    });\n  }\n\n  Future<File> _getLogsFile() async {\n    final directory = await getApplicationSupportDirectory();\n    final path = directory.path;\n    return File('$path/logs/kazumi_logs.log');\n  }\n\n  Future<void> _clearLogs() async {\n    try {\n      final file = await _getLogsFile();\n      await file.writeAsString('');\n      if (!mounted) return;\n      \n      setState(() {\n        _logLines.clear();\n        _allLines.clear();\n        _fullContent = '';\n        _displayedLines = 0;\n      });\n    } catch (e) {\n      if (!mounted) return;\n      KazumiDialog.showToast(message: '清空失败: $e');\n    }\n  }\n\n  Future<void> _copyLogs() async {\n    try {\n      await Clipboard.setData(ClipboardData(text: _fullContent));\n      if (!mounted) return;\n      KazumiDialog.showToast(message: '已复制到剪贴板');\n    } catch (e) {\n      if (!mounted) return;\n      KazumiDialog.showToast(message: '复制失败: $e');\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('日志'),\n      ),\n      body: buildBody,\n      floatingActionButton: buildFloatingButtons,\n    );\n  }\n\n  Widget get buildBody {\n    if (_isLoading) {\n      return const Center(\n        child: CircularProgressIndicator(),\n      );\n    }\n    \n    if (_hasError) {\n      return const Center(\n        child: Text('加载日志失败'),\n      );\n    }\n    \n    if (_logLines.isEmpty) {\n      return const Center(\n        child: Text('没有数据'),\n      );\n    }\n    \n    return SelectionArea(\n      child: SingleChildScrollView(\n        scrollDirection: Axis.horizontal,\n        child: SizedBox(\n          width: MediaQuery.of(context).size.width.clamp(600, double.infinity),\n          child: ListView.builder(\n            controller: _scrollController,\n            padding: const EdgeInsets.all(16.0),\n            shrinkWrap: false,\n            itemCount: _logLines.length,\n            itemBuilder: (context, index) {\n              return Padding(\n                padding: const EdgeInsets.only(bottom: 4.0),\n                child: Text(\n                  _logLines[index],\n                  softWrap: false,\n                  overflow: TextOverflow.clip,\n                  style: const TextStyle(\n                    fontFamily: 'monospace',\n                    fontSize: 12,\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget get buildFloatingButtons {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        FloatingActionButton(\n          heroTag: null,\n          onPressed: _clearLogs,\n          tooltip: '清空日志',\n          child: const Icon(Icons.clear_all),\n        ),\n        const SizedBox(width: 15),\n        FloatingActionButton(\n          heroTag: null,\n          onPressed: _copyLogs,\n          tooltip: '复制日志',\n          child: const Icon(Icons.copy),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/menu/menu.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/pages/router.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:provider/provider.dart';\n\nclass ScaffoldMenu extends StatefulWidget {\n  const ScaffoldMenu({super.key});\n\n  @override\n  State<ScaffoldMenu> createState() => _ScaffoldMenu();\n}\n\nclass NavigationBarState extends ChangeNotifier {\n  late int _selectedIndex = getDefaultSelectedIndex();\n  bool _isHide = false;\n  bool _isBottom = false;\n\n  int get selectedIndex => _selectedIndex;\n\n  bool get isHide => _isHide;\n\n  bool get isBottom => _isBottom;\n\n  int getDefaultSelectedIndex() {\n    final defaultPage = GStorage.setting\n        .get(SettingBoxKey.defaultStartupPage, defaultValue: \"/tab/popular/\");\n\n    switch (defaultPage) {\n      case \"/tab/popular/\":\n        return 0;\n      case \"/tab/timeline/\":\n        return 1;\n      case \"/tab/collect/\":\n        return 2;\n      case \"/tab/my/\":\n        return 3;\n      default:\n        return 0;\n    }\n  }\n\n  void updateSelectedIndex(int pageIndex) {\n    _selectedIndex = pageIndex;\n    notifyListeners();\n  }\n\n  void hideNavigate() {\n    _isHide = true;\n    notifyListeners();\n  }\n\n  void showNavigate() {\n    _isHide = false;\n    notifyListeners();\n  }\n}\n\nclass _ScaffoldMenu extends State<ScaffoldMenu> {\n  final PageController _page = PageController();\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider(\n        create: (context) => NavigationBarState(),\n        child: Consumer<NavigationBarState>(builder: (context, state, _) {\n          return OrientationBuilder(builder: (context, orientation) {\n            state._isBottom = orientation == Orientation.portrait;\n            return orientation != Orientation.portrait\n                ? sideMenuWidget(context, state)\n                : bottomMenuWidget(context, state);\n          });\n        }));\n  }\n\n  Widget bottomMenuWidget(BuildContext context, NavigationBarState state) {\n    return Scaffold(\n        body: Container(\n          color: Theme.of(context).colorScheme.primaryContainer,\n          child: PageView.builder(\n            physics: const NeverScrollableScrollPhysics(),\n            controller: _page,\n            itemCount: menu.size,\n            itemBuilder: (_, __) => const RouterOutlet(),\n          ),\n        ),\n        bottomNavigationBar: state.isHide\n            ? const SizedBox(height: 0)\n            : NavigationBar(\n                destinations: const <Widget>[\n                  NavigationDestination(\n                    selectedIcon: Icon(Icons.home),\n                    icon: Icon(Icons.home_outlined),\n                    label: '推荐',\n                  ),\n                  NavigationDestination(\n                    selectedIcon: Icon(Icons.timeline),\n                    icon: Icon(Icons.timeline_outlined),\n                    label: '时间表',\n                  ),\n                  NavigationDestination(\n                    selectedIcon: Icon(Icons.favorite),\n                    icon: Icon(Icons.favorite_outlined),\n                    label: '追番',\n                  ),\n                  NavigationDestination(\n                    selectedIcon: Icon(Icons.settings),\n                    icon: Icon(Icons.settings),\n                    label: '我的',\n                  ),\n                ],\n                selectedIndex: state.selectedIndex,\n                onDestinationSelected: (int index) {\n                  state.updateSelectedIndex(index);\n                  Modular.to.navigate(\"/tab${menu.getPath(index)}/\");\n                },\n              ));\n  }\n\n  Widget sideMenuWidget(BuildContext context, NavigationBarState state) {\n    return Scaffold(\n      backgroundColor: Theme.of(context).colorScheme.surfaceContainer,\n      body: Row(\n        children: [\n          EmbeddedNativeControlArea(\n            child: Visibility(\n              visible: !state.isHide,\n              child: NavigationRail(\n                backgroundColor: Theme.of(context).colorScheme.surfaceContainer,\n                groupAlignment: 1.0,\n                leading: FloatingActionButton(\n                  elevation: 0,\n                  heroTag: null,\n                  onPressed: () {\n                    Modular.to.pushNamed('/search/');\n                  },\n                  child: const Icon(Icons.search),\n                ),\n                labelType: NavigationRailLabelType.selected,\n                destinations: const <NavigationRailDestination>[\n                  NavigationRailDestination(\n                    selectedIcon: Icon(Icons.home),\n                    icon: Icon(Icons.home_outlined),\n                    label: Text('推荐'),\n                  ),\n                  NavigationRailDestination(\n                    selectedIcon: Icon(Icons.timeline),\n                    icon: Icon(Icons.timeline_outlined),\n                    label: Text('时间表'),\n                  ),\n                  NavigationRailDestination(\n                    selectedIcon: Icon(Icons.favorite),\n                    icon: Icon(Icons.favorite_border),\n                    label: Text('追番'),\n                  ),\n                  NavigationRailDestination(\n                    selectedIcon: Icon(Icons.settings),\n                    icon: Icon(Icons.settings_outlined),\n                    label: Text('我的'),\n                  ),\n                ],\n                selectedIndex: state.selectedIndex,\n                onDestinationSelected: (int index) {\n                  state.updateSelectedIndex(index);\n                  Modular.to.navigate(\"/tab${menu.getPath(index)}/\");\n                },\n              ),\n            ),\n          ),\n          Expanded(\n            child: Container(\n              decoration: BoxDecoration(\n                color: Theme.of(context).colorScheme.primaryContainer,\n                borderRadius: const BorderRadius.only(\n                  topLeft: Radius.circular(16.0),\n                  bottomLeft: Radius.circular(16.0),\n                ),\n              ),\n              child: ClipRRect(\n                borderRadius: const BorderRadius.only(\n                  topLeft: Radius.circular(16.0),\n                  bottomLeft: Radius.circular(16.0),\n                ),\n                child: PageView.builder(\n                  physics: const NeverScrollableScrollPhysics(),\n                  itemCount: menu.size,\n                  itemBuilder: (_, __) => const RouterOutlet(),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}"
  },
  {
    "path": "lib/pages/my/my_controller.dart",
    "content": "import 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/auto_updater.dart';\n\npart 'my_controller.g.dart';\n\nclass MyController = _MyController with _$MyController;\n\nabstract class _MyController with Store {\n  Box setting = GStorage.setting;\n\n  @observable\n  ObservableList<String> shieldList = ObservableList.of([]);\n\n  bool isDanmakuBlocked(String? danmaku) {\n    if (danmaku == null || danmaku.isEmpty) return false;\n    for (String item in shieldList) {\n      if (item.isEmpty) continue;\n      if (item.startsWith('/') && item.endsWith('/')) {\n        if (item.length <= 2) continue;\n        String pattern = item.substring(1, item.length - 1);\n        try {\n          if (RegExp(pattern).hasMatch(danmaku)) return true;\n        } catch (_) {\n          KazumiLogger().e('Danmaku: invalid danmaku shield regex pattern: $pattern');\n          continue;\n        }\n      } else {\n        if (danmaku.contains(item)) return true;\n      }\n    }\n    return false;\n  }\n\n  void loadShieldList() {\n    shieldList.clear();\n    shieldList.addAll(GStorage.shieldList.values.toList());\n  }\n\n  void addShieldList(String item) {\n    if (item.isEmpty) {\n      KazumiDialog.showToast(message: '请输入关键词');\n      return;\n    }\n    if (item.length > 64) {\n      KazumiDialog.showToast(message: '关键词过长');\n      return;\n    }\n    if (shieldList.contains(item)) {\n      KazumiDialog.showToast(message: '已存在该关键词');\n      return;\n    }\n    shieldList.add(item);\n    GStorage.shieldList.put(item, item);\n    GStorage.shieldList.flush();\n  }\n\n  void removeShieldList(String item) {\n    shieldList.remove(item);\n    GStorage.shieldList.delete(item);\n    GStorage.shieldList.flush();\n  }\n\n  Future<bool> checkUpdate({String type = 'manual'}) async {\n    try {\n      final autoUpdater = AutoUpdater();\n\n      if (type == 'manual') {\n        await autoUpdater.manualCheckForUpdates();\n      } else {\n        await autoUpdater.autoCheckForUpdates();\n      }\n\n      return true;\n    } catch (err) {\n      KazumiLogger().e('Update: check update failed', error: err);\n      if (type == 'manual') {\n        KazumiDialog.showToast(message: '检查更新失败，请稍后重试');\n      }\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/my/my_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'my_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$MyController on _MyController, Store {\n  late final _$shieldListAtom =\n      Atom(name: '_MyController.shieldList', context: context);\n\n  @override\n  ObservableList<String> get shieldList {\n    _$shieldListAtom.reportRead();\n    return super.shieldList;\n  }\n\n  @override\n  set shieldList(ObservableList<String> value) {\n    _$shieldListAtom.reportWrite(value, super.shieldList, () {\n      super.shieldList = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\nshieldList: ${shieldList}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/my/my_module.dart",
    "content": "import 'package:kazumi/pages/my/my_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass MyModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const MyPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/my/my_page.dart",
    "content": "import 'package:card_settings_ui/card_settings_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/pages/menu/menu.dart';\nimport 'package:provider/provider.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\n\nclass MyPage extends StatefulWidget {\n  const MyPage({super.key});\n\n  @override\n  State<MyPage> createState() => _MyPageState();\n}\n\nclass _MyPageState extends State<MyPage> {\n  late NavigationBarState navigationBarState;\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n    navigationBarState.updateSelectedIndex(0);\n    Modular.to.navigate('/tab/popular/');\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    navigationBarState =\n        Provider.of<NavigationBarState>(context, listen: false);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('我的'), needTopOffset: false),\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              title: Text('播放历史与视频源', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/history/');\n                  },\n                  leading: const Icon(Icons.history_rounded),\n                  title: Text('历史记录', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('查看播放历史记录',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/download/');\n                  },\n                  leading: const Icon(Icons.download_rounded),\n                  title: Text('下载管理', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('查看和管理离线下载',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/download-settings');\n                  },\n                  leading: const Icon(Icons.settings_rounded),\n                  title: Text('下载设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('配置下载并发数等参数',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/plugin/');\n                  },\n                  leading: const Icon(Icons.extension),\n                  title: Text('规则管理', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('管理番剧资源规则',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('播放器设置', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/player');\n                  },\n                  leading: const Icon(Icons.display_settings_rounded),\n                  title: Text('播放设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('设置播放器相关参数',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/danmaku/');\n                  },\n                  leading: const Icon(Icons.subtitles_rounded),\n                  title: Text('弹幕设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('设置弹幕相关参数',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/keyboard');\n                  },\n                  leading: const Icon(Icons.keyboard_rounded),\n                  title: Text('操作设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('设置播放器按键映射',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/proxy');\n                  },\n                  leading: const Icon(Icons.vpn_key_rounded),\n                  title: Text('代理设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('配置HTTP代理',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('应用与外观', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/theme');\n                  },\n                  leading: const Icon(Icons.palette_rounded),\n                  title: Text('外观设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('设置应用主题和刷新率',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/interface');\n                  },\n                  leading: const Icon(Icons.pages_rounded),\n                  title: Text('界面设置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('设置应用界面样式',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/webdav/');\n                  },\n                  leading: const Icon(Icons.cloud),\n                  title: Text('同步设置', style: TextStyle(fontFamily: fontFamily)),\n                  description:\n                      Text('设置同步参数', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('其他', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/about/');\n                  },\n                  leading: const Icon(Icons.info_outline_rounded),\n                  title: Text('关于', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/episode_comments_sheet.dart",
    "content": "import 'dart:ui';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/card/episode_comments_card.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\n\nclass EpisodeInfo extends InheritedWidget {\n  /// This widget receives changes of episode and notify it's child,\n  /// trigger [didChangeDependencies] of it's child.\n  const EpisodeInfo({super.key, required this.episode, required super.child});\n\n  final int episode;\n\n  @override\n  bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;\n\n  static EpisodeInfo? of(BuildContext context) {\n    return context.dependOnInheritedWidgetOfExactType<EpisodeInfo>();\n  }\n}\n\nclass EpisodeCommentsSheet extends StatefulWidget {\n  const EpisodeCommentsSheet({super.key});\n\n  @override\n  State<EpisodeCommentsSheet> createState() => _EpisodeCommentsSheetState();\n}\n\nclass _EpisodeCommentsSheetState extends State<EpisodeCommentsSheet> {\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  bool commentsQueryTimeout = false;\n  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =\n      GlobalKey<RefreshIndicatorState>();\n\n  /// episode input by [showEpisodeSelection]\n  int ep = 0;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  Future<void> loadComments(int episode) async {\n    commentsQueryTimeout = false;\n    await videoPageController\n        .queryBangumiEpisodeCommentsByID(\n            videoPageController.bangumiItem.id, episode)\n        .then((_) {\n      if (videoPageController.episodeCommentsList.isEmpty && mounted) {\n        setState(() {\n          commentsQueryTimeout = true;\n        });\n      }\n    });\n    if (mounted) {\n      setState(() {});\n    }\n  }\n\n  void toggleSortOrder() {\n    videoPageController.toggleSortOrder();\n  }\n\n  @override\n  void didChangeDependencies() {\n    ep = 0;\n    // wait until currentState is not null\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (videoPageController.episodeCommentsList.isEmpty) {\n        // trigger RefreshIndicator onRefresh and show animation\n        _refreshIndicatorKey.currentState?.show();\n      }\n    });\n    super.didChangeDependencies();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  Widget get episodeCommentsBody {\n    return CustomScrollView(\n      scrollBehavior: const ScrollBehavior().copyWith(\n        // Scrollbars' movement is not linear so hide it.\n        scrollbars: false,\n        // Enable mouse drag to refresh\n        dragDevices: {\n          PointerDeviceKind.mouse,\n          PointerDeviceKind.touch,\n          PointerDeviceKind.trackpad\n        },\n      ),\n      slivers: [\n        SliverPadding(\n          padding: const EdgeInsets.fromLTRB(4, 0, 4, 4),\n          sliver: Observer(builder: (context) {\n            if (commentsQueryTimeout) {\n              return const SliverFillRemaining(\n                child: Center(\n                  child: Text('空空如也'),\n                ),\n              );\n            }\n            return SliverList(\n              delegate: SliverChildBuilderDelegate(\n                (context, index) {\n                  // Fix scroll issue caused by height change of network images\n                  // by keeping loaded cards alive.\n                  return KeepAlive(\n                    keepAlive: true,\n                    child: IndexedSemantics(\n                      index: index,\n                      child: SelectionArea(\n                        child: EpisodeCommentsCard(\n                          commentItem:\n                              videoPageController.episodeCommentsList[index],\n                        ),\n                      ),\n                    ),\n                  );\n                },\n                childCount: videoPageController.episodeCommentsList.length,\n                addAutomaticKeepAlives: false,\n                addRepaintBoundaries: false,\n                addSemanticIndexes: false,\n              ),\n            );\n          }),\n        ),\n      ],\n    );\n  }\n\n  Widget get commentsInfo {\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          const Text(' 本集标题  '),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                    '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.name}',\n                    overflow: TextOverflow.ellipsis,\n                    style: TextStyle(\n                        fontSize: 12,\n                        color: Theme.of(context).colorScheme.outline)),\n                Text(\n                    (videoPageController.episodeInfo.nameCn != '')\n                        ? '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.nameCn}'\n                        : '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.name}',\n                    overflow: TextOverflow.ellipsis,\n                    style: TextStyle(\n                        fontSize: 12,\n                        color: Theme.of(context).colorScheme.outline)),\n              ],\n            ),\n          ),\n          const SizedBox(width: 10),\n          SizedBox(\n            height: 34,\n            child: TextButton(\n              style: ButtonStyle(\n                padding: WidgetStateProperty.all(\n                    const EdgeInsets.only(left: 4.0, right: 4.0)),\n              ),\n              onPressed: () {\n                showEpisodeSelection();\n              },\n              child: const Text(\n                '手动切换',\n                style: TextStyle(fontSize: 13),\n              ),\n            ),\n          ),\n          SizedBox(\n            height: 34,\n            child: TextButton(\n              style: ButtonStyle(\n                padding: WidgetStateProperty.all(\n                    const EdgeInsets.symmetric(horizontal: 4.0)),\n              ),\n              onPressed: toggleSortOrder,\n              child: Observer(builder: (context) {\n                return Text(\n                  videoPageController.isCommentsAscending ? '倒序' : '正序',\n                  style: const TextStyle(fontSize: 13),\n                );\n              }),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  // 选择要查看评论的集数\n  void showEpisodeSelection() {\n    final TextEditingController textController = TextEditingController();\n    KazumiDialog.show(\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('输入集数'),\n          content: StatefulBuilder(\n              builder: (BuildContext context, StateSetter setState) {\n            return TextField(\n              inputFormatters: <TextInputFormatter>[\n                FilteringTextInputFormatter.digitsOnly\n              ],\n              controller: textController,\n            );\n          }),\n          actions: [\n            TextButton(\n              onPressed: () => KazumiDialog.dismiss(),\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () {\n                if (textController.text.isEmpty) {\n                  KazumiDialog.showToast(message: '请输入集数');\n                  return;\n                }\n                ep = int.tryParse(textController.text) ?? 0;\n                if (ep == 0) {\n                  return;\n                }\n                _refreshIndicatorKey.currentState?.show();\n                KazumiDialog.dismiss();\n              },\n              child: const Text('刷新'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final int episode = EpisodeInfo.of(context)!.episode;\n    return Scaffold(\n      body: RefreshIndicator(\n        key: _refreshIndicatorKey,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [commentsInfo, Expanded(child: episodeCommentsBody)],\n        ),\n        onRefresh: () async {\n          await loadComments(ep == 0 ? episode : ep);\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/player_controller.dart",
    "content": "import 'dart:io';\nimport 'dart:async';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_volume_controller/flutter_volume_controller.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:media_kit/media_kit.dart';\nimport 'package:media_kit_video/media_kit_video.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_module.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:canvas_danmaku/canvas_danmaku.dart';\nimport 'package:kazumi/request/damaku.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/shaders/shaders_controller.dart';\nimport 'package:kazumi/utils/syncplay.dart';\nimport 'package:kazumi/utils/syncplay_endpoint.dart';\nimport 'package:kazumi/utils/external_player.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\n\npart 'player_controller.g.dart';\n\nclass PlaybackInitParams {\n  final String videoUrl;\n  final int offset;\n  final bool isLocalPlayback;\n  final int bangumiId;\n  final String pluginName;\n  final int episode;\n  final Map<String, String> httpHeaders;\n  final bool adBlockerEnabled;\n  final String episodeTitle;\n  final String referer;\n  final int currentRoad;\n\n  const PlaybackInitParams({\n    required this.videoUrl,\n    required this.offset,\n    required this.isLocalPlayback,\n    required this.bangumiId,\n    required this.pluginName,\n    required this.episode,\n    required this.httpHeaders,\n    required this.adBlockerEnabled,\n    required this.episodeTitle,\n    required this.referer,\n    required this.currentRoad,\n  });\n}\n\nenum DanmakuDestination {\n  chatRoom,\n  remoteDanmaku,\n}\n\nclass SyncPlayChatMessage {\n  final String username;\n  final String message;\n  final bool fromRemote;\n  final DateTime time;\n\n  SyncPlayChatMessage({\n    required this.username,\n    required this.message,\n    this.fromRemote = true,\n    DateTime? time,\n  }) : time = time ?? DateTime.now();\n}\n\nclass PlayerController = _PlayerController with _$PlayerController;\n\nabstract class _PlayerController with Store {\n  final ShadersController shadersController = Modular.get<ShadersController>();\n\n  late int bangumiId;\n  late int currentEpisode;\n  late int currentRoad;\n  late String referer;\n\n  // 弹幕控制\n  late DanmakuController danmakuController;\n  @observable\n  Map<int, List<Danmaku>> danDanmakus = {};\n  @observable\n  bool danmakuOn = false;\n  @observable\n  bool danmakuLoading = false;\n  DanmakuDestination danmakuDestination = DanmakuDestination.remoteDanmaku;\n  final StreamController<SyncPlayChatMessage> syncPlayChatStreamController =\n      StreamController<SyncPlayChatMessage>.broadcast();\n  Stream<SyncPlayChatMessage> get syncPlayChatStream =>\n      syncPlayChatStreamController.stream;\n\n  // 一起看控制器\n  SyncplayClient? syncplayController;\n  @observable\n  String syncplayRoom = '';\n  @observable\n  int syncplayClientRtt = 0;\n\n  /// 视频比例类型\n  /// 1. AUTO\n  /// 2. COVER\n  /// 3. FILL\n  @observable\n  int aspectRatioType = 1;\n\n  /// 视频超分\n  /// 1. OFF\n  /// 2. Anime4K Efficiency\n  /// 3. Anime4K Quality\n  @observable\n  int superResolutionType = 1;\n\n  // 视频音量/亮度\n  @observable\n  double volume = -1;\n  @observable\n  double brightness = 0;\n\n  // 播放器界面控制\n  @observable\n  bool lockPanel = false;\n  @observable\n  bool showVideoController = true;\n  @observable\n  bool showSeekTime = false;\n  @observable\n  bool showBrightness = false;\n  @observable\n  bool showVolume = false;\n  @observable\n  bool showPlaySpeed = false;\n  @observable\n  bool brightnessSeeking = false;\n  @observable\n  bool volumeSeeking = false;\n  @observable\n  bool canHidePlayerPanel = true;\n\n  // 视频地址\n  String videoUrl = '';\n\n  // DanDanPlay 弹幕ID\n  int bangumiID = 0;\n\n  // 播放器实体\n  Player? mediaPlayer;\n  VideoController? videoController;\n\n  // 播放器面板状态\n  @observable\n  bool loading = true;\n  @observable\n  bool playing = false;\n  @observable\n  bool isBuffering = true;\n  @observable\n  bool completed = false;\n  @observable\n  Duration currentPosition = Duration.zero;\n  @observable\n  Duration buffer = Duration.zero;\n  @observable\n  Duration duration = Duration.zero;\n  @observable\n  double playerSpeed = 1.0;\n\n  Box setting = GStorage.setting;\n  bool hAenable = true;\n  late String hardwareDecoder;\n  bool androidEnableOpenSLES = true;\n  bool lowMemoryMode = false;\n  bool autoPlay = true;\n  bool playerDebugMode = false;\n  int buttonSkipTime = 80;\n  int arrowKeySkipTime = 10;\n\n  // 播放器实时状态\n  bool get playerPlaying => mediaPlayer!.state.playing;\n  bool get playerBuffering => mediaPlayer!.state.buffering;\n  bool get playerCompleted => mediaPlayer!.state.completed;\n  double get playerVolume => mediaPlayer!.state.volume;\n  Duration get playerPosition => mediaPlayer!.state.position;\n  Duration get playerBuffer => mediaPlayer!.state.buffer;\n  Duration get playerDuration => mediaPlayer!.state.duration;\n\n  // 播放器调试信息\n  /// LogLevel 0: 错误 1: 警告 2: 简略 3: 详细\n  int playerLogLevel = 2;\n  @observable\n  ObservableList<String> playerLog = ObservableList.of([]);\n  @observable\n  int playerWidth = 0;\n  @observable\n  int playerHeight = 0;\n  @observable\n  String playerVideoParams = '';\n  @observable\n  String playerAudioParams = '';\n  @observable\n  String playerPlaylist = '';\n  @observable\n  String playerAudioTracks = '';\n  @observable\n  String playerVideoTracks = '';\n  @observable\n  String playerAudioBitrate = '';\n\n  /// 播放器调试信息订阅\n  StreamSubscription<PlayerLog>? playerLogSubscription;\n  StreamSubscription<int?>? playerWidthSubscription;\n  StreamSubscription<int?>? playerHeightSubscription;\n  StreamSubscription<VideoParams>? playerVideoParamsSubscription;\n  StreamSubscription<AudioParams>? playerAudioParamsSubscription;\n  StreamSubscription<Playlist>? playerPlaylistSubscription;\n  StreamSubscription<Track>? playerTracksSubscription;\n  StreamSubscription<double?>? playerAudioBitrateSubscription;\n\n  bool isLocalPlayback = false;\n\n  Future<void> init(PlaybackInitParams params) async {\n    videoUrl = params.videoUrl;\n    isLocalPlayback = params.isLocalPlayback;\n    bangumiId = params.bangumiId;\n    currentEpisode = params.episode;\n    currentRoad = params.currentRoad;\n    referer = params.referer;\n\n    KazumiLogger().i(\n        'PlayerController: ${params.isLocalPlayback ? \"local\" : \"online\"} playback, url: ${params.videoUrl}');\n\n    playing = false;\n    loading = true;\n    isBuffering = true;\n    currentPosition = Duration.zero;\n    buffer = Duration.zero;\n    duration = Duration.zero;\n    completed = false;\n    playerLogLevel = setting.get(SettingBoxKey.playerLogLevel, defaultValue: 2);\n    playerSpeed =\n        setting.get(SettingBoxKey.defaultPlaySpeed, defaultValue: 1.0);\n    aspectRatioType =\n        setting.get(SettingBoxKey.defaultAspectRatioType, defaultValue: 1);\n\n    buttonSkipTime =\n        setting.get(SettingBoxKey.buttonSkipTime, defaultValue: 80);\n    arrowKeySkipTime =\n        setting.get(SettingBoxKey.arrowKeySkipTime, defaultValue: 10);\n    try {\n      await dispose(disposeSyncPlayController: false);\n    } catch (_) {}\n    int episodeFromTitle = 0;\n    try {\n      episodeFromTitle = Utils.extractEpisodeNumber(params.episodeTitle);\n    } catch (e) {\n      KazumiLogger().e(\n          'PlayerController: failed to extract episode number from title',\n          error: e);\n    }\n    if (episodeFromTitle == 0) {\n      episodeFromTitle = params.episode;\n    }\n    _loadDanmaku(params.bangumiId, params.pluginName, episodeFromTitle);\n    mediaPlayer ??= await createVideoController(\n      params.httpHeaders,\n      params.adBlockerEnabled,\n      offset: params.offset,\n    );\n\n    if (Utils.isDesktop()) {\n      volume = volume != -1 ? volume : 100;\n      await setVolume(volume);\n    } else {\n      // mobile is using system volume, don't setVolume here,\n      // or iOS will mute if system volume is too low (#732)\n      await FlutterVolumeController.getVolume().then((value) {\n        volume = (value ?? 0.0) * 100;\n      });\n    }\n    setPlaybackSpeed(playerSpeed);\n    KazumiLogger().i('PlayerController: video initialized');\n    loading = false;\n    if (syncplayController?.isConnected ?? false) {\n      if (syncplayController!.currentFileName !=\n          \"$bangumiId[$currentEpisode]\") {\n        setSyncPlayPlayingBangumi(\n            forceSyncPlaying: true, forceSyncPosition: 0.0);\n      }\n    }\n  }\n\n  Future<void> setupPlayerDebugInfoSubscription() async {\n    await playerLogSubscription?.cancel();\n    playerLogSubscription = mediaPlayer!.stream.log.listen((event) {\n      playerLog.add(event.toString());\n      if (playerDebugMode) {\n        KazumiLogger().i(\"MPV: ${event.toString()}\", forceLog: true);\n      }\n    });\n    await playerWidthSubscription?.cancel();\n    playerWidthSubscription = mediaPlayer!.stream.width.listen((event) {\n      playerWidth = event ?? 0;\n    });\n    await playerHeightSubscription?.cancel();\n    playerHeightSubscription = mediaPlayer!.stream.height.listen((event) {\n      playerHeight = event ?? 0;\n    });\n    await playerVideoParamsSubscription?.cancel();\n    playerVideoParamsSubscription =\n        mediaPlayer!.stream.videoParams.listen((event) {\n      playerVideoParams = event.toString();\n    });\n    await playerAudioParamsSubscription?.cancel();\n    playerAudioParamsSubscription =\n        mediaPlayer!.stream.audioParams.listen((event) {\n      playerAudioParams = event.toString();\n    });\n    await playerPlaylistSubscription?.cancel();\n    playerPlaylistSubscription = mediaPlayer!.stream.playlist.listen((event) {\n      playerPlaylist = event.toString();\n    });\n    await playerTracksSubscription?.cancel();\n    playerTracksSubscription = mediaPlayer!.stream.track.listen((event) {\n      playerAudioTracks = event.audio.toString();\n      playerVideoTracks = event.video.toString();\n    });\n    await playerAudioBitrateSubscription?.cancel();\n    playerAudioBitrateSubscription =\n        mediaPlayer!.stream.audioBitrate.listen((event) {\n      playerAudioBitrate = event.toString();\n    });\n  }\n\n  Future<void> cancelPlayerDebugInfoSubscription() async {\n    await playerLogSubscription?.cancel();\n    await playerWidthSubscription?.cancel();\n    await playerHeightSubscription?.cancel();\n    await playerVideoParamsSubscription?.cancel();\n    await playerAudioParamsSubscription?.cancel();\n    await playerPlaylistSubscription?.cancel();\n    await playerTracksSubscription?.cancel();\n    await playerAudioBitrateSubscription?.cancel();\n  }\n\n  Future<Player> createVideoController(\n      Map<String, String> httpHeaders, bool adBlockerEnabled,\n      {int offset = 0}) async {\n    superResolutionType =\n        setting.get(SettingBoxKey.defaultSuperResolutionType, defaultValue: 1);\n    hAenable = setting.get(SettingBoxKey.hAenable, defaultValue: true);\n    androidEnableOpenSLES =\n        setting.get(SettingBoxKey.androidEnableOpenSLES, defaultValue: true);\n    hardwareDecoder =\n        setting.get(SettingBoxKey.hardwareDecoder, defaultValue: 'auto-safe');\n    autoPlay = setting.get(SettingBoxKey.autoPlay, defaultValue: true);\n    lowMemoryMode =\n        setting.get(SettingBoxKey.lowMemoryMode, defaultValue: false);\n    playerDebugMode =\n        setting.get(SettingBoxKey.playerDebugMode, defaultValue: false);\n\n    mediaPlayer = Player(\n      configuration: PlayerConfiguration(\n        bufferSize: lowMemoryMode ? 15 * 1024 * 1024 : 1500 * 1024 * 1024,\n        osc: false,\n        logLevel: MPVLogLevel.values[playerLogLevel],\n        adBlocker: adBlockerEnabled,\n      ),\n    );\n\n    playerLog.clear();\n    setupPlayerDebugInfoSubscription();\n\n    var pp = mediaPlayer!.platform as NativePlayer;\n    // media-kit 默认启用硬盘作为双重缓存，这可以维持大缓存的前提下减轻内存压力\n    // media-kit 内部硬盘缓存目录按照 Linux 配置，这导致该功能在其他平台上被损坏\n    // 该设置可以在所有平台上正确启用双重缓存\n    await pp.setProperty(\"demuxer-cache-dir\", await Utils.getPlayerTempPath());\n    await pp.setProperty(\"af\", \"scaletempo2=max-speed=8\");\n    if (Platform.isAndroid) {\n      await pp.setProperty(\"volume-max\", \"100\");\n      if (androidEnableOpenSLES) {\n        await pp.setProperty(\"ao\", \"opensles\");\n      } else {\n        await pp.setProperty(\"ao\", \"audiotrack\");\n      }\n    }\n\n    // 设置 HTTP 代理\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (proxyEnable) {\n      final String proxyUrl =\n          setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n      final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl);\n      if (formattedProxy != null) {\n        await pp.setProperty(\"http-proxy\", formattedProxy);\n        KazumiLogger().i('Player: HTTP 代理设置成功 $formattedProxy');\n      }\n    }\n\n    await mediaPlayer!.setAudioTrack(\n      AudioTrack.auto(),\n    );\n\n    String? videoRenderer;\n    if (Platform.isAndroid) {\n      final String androidVideoRenderer =\n          setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto');\n\n      if (androidVideoRenderer == 'auto') {\n        // Android 14 及以上使用基于 Vulkan 的 MPV GPU-NEXT 视频输出，着色器性能更好\n        // GPU-NEXT 需要 Vulkan 1.2 支持\n        // 避免 Android 14 及以下设备上部分机型 Vulkan 支持不佳导致的黑屏问题\n        final int androidSdkVersion = await Utils.getAndroidSdkVersion();\n        if (androidSdkVersion >= 34) {\n          videoRenderer = 'gpu-next';\n        } else {\n          videoRenderer = 'gpu';\n        }\n      } else {\n        videoRenderer = androidVideoRenderer;\n      }\n    }\n\n    if (videoRenderer == 'mediacodec_embed') {\n      hAenable = true;\n      hardwareDecoder = 'mediacodec';\n      superResolutionType = 1;\n    }\n\n    videoController ??= VideoController(\n      mediaPlayer!,\n      configuration: VideoControllerConfiguration(\n        vo: videoRenderer,\n        enableHardwareAcceleration: hAenable,\n        hwdec: hAenable ? hardwareDecoder : 'no',\n        androidAttachSurfaceAfterVideoParameters: false,\n      ),\n    );\n    mediaPlayer!.setPlaylistMode(PlaylistMode.none);\n\n    // error handle\n    bool showPlayerError =\n        setting.get(SettingBoxKey.showPlayerError, defaultValue: true);\n    mediaPlayer!.stream.error.listen((event) {\n      if (showPlayerError) {\n        if (event.toString().contains('Failed to open') && playerBuffering) {\n          KazumiDialog.showToast(\n              message: '加载失败, 请尝试更换其他视频来源',\n              showActionButton: true);\n        } else {\n          KazumiDialog.showToast(\n              message: '播放器内部错误 ${event.toString()} $videoUrl',\n              duration: const Duration(seconds: 5),\n              showActionButton: true);\n        }\n      }\n      KazumiLogger()\n          .e('PlayerController: Player intent error $videoUrl', error: event);\n    });\n\n    if (superResolutionType != 1) {\n      await setShader(superResolutionType);\n    }\n\n    await mediaPlayer!.open(\n      Media(videoUrl,\n          start: Duration(seconds: offset), httpHeaders: httpHeaders),\n      play: autoPlay,\n    );\n\n    return mediaPlayer!;\n  }\n\n  Future<void> setShader(int type, {bool synchronized = true}) async {\n    var pp = mediaPlayer!.platform as NativePlayer;\n    await pp.waitForPlayerInitialization;\n    await pp.waitForVideoControllerInitializationIfAttached;\n    if (type == 2) {\n      await pp.command([\n        'change-list',\n        'glsl-shaders',\n        'set',\n        Utils.buildShadersAbsolutePath(\n            shadersController.shadersDirectory.path, mpvAnime4KShadersLite),\n      ]);\n      superResolutionType = 2;\n      return;\n    }\n    if (type == 3) {\n      await pp.command([\n        'change-list',\n        'glsl-shaders',\n        'set',\n        Utils.buildShadersAbsolutePath(\n            shadersController.shadersDirectory.path, mpvAnime4KShaders),\n      ]);\n      superResolutionType = 3;\n      return;\n    }\n    await pp.command(['change-list', 'glsl-shaders', 'clr', '']);\n    superResolutionType = 1;\n  }\n\n  Future<void> setPlaybackSpeed(double playerSpeed) async {\n    this.playerSpeed = playerSpeed;\n    try {\n      mediaPlayer!.setRate(playerSpeed);\n    } catch (e) {\n      KazumiLogger()\n          .e('PlayerController: failed to set playback speed', error: e);\n    }\n    try {\n      updateDanmakuSpeed();\n    } catch (_) {}\n  }\n\n  void updateDanmakuSpeed() {\n    final baseDuration =\n        setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0);\n    final followSpeed =\n        setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true);\n\n    final duration = followSpeed ? (baseDuration / playerSpeed) : baseDuration;\n    danmakuController\n        .updateOption(danmakuController.option.copyWith(duration: duration));\n  }\n\n  Future<void> setVolume(double value) async {\n    value = value.clamp(0.0, 100.0);\n    volume = value;\n    try {\n      if (Utils.isDesktop()) {\n        await mediaPlayer!.setVolume(value);\n      } else {\n        await FlutterVolumeController.updateShowSystemUI(false);\n        await FlutterVolumeController.setVolume(value / 100);\n      }\n    } catch (_) {}\n  }\n\n  Future<void> playOrPause() async {\n    if (mediaPlayer!.state.playing) {\n      await pause();\n    } else {\n      await play();\n    }\n  }\n\n  Future<void> seek(Duration duration, {bool enableSync = true}) async {\n    currentPosition = duration;\n    danmakuController.clear();\n    await mediaPlayer!.seek(duration);\n    if (syncplayController != null) {\n      setSyncPlayCurrentPosition();\n      if (enableSync) {\n        await requestSyncPlaySync(doSeek: true);\n      }\n    }\n  }\n\n  Future<void> pause({bool enableSync = true}) async {\n    danmakuController.pause();\n    await mediaPlayer!.pause();\n    playing = false;\n    if (syncplayController != null) {\n      setSyncPlayCurrentPosition();\n      if (enableSync) {\n        await requestSyncPlaySync();\n      }\n    }\n  }\n\n  Future<void> play({bool enableSync = true}) async {\n    danmakuController.resume();\n    await mediaPlayer!.play();\n    playing = true;\n    if (syncplayController != null) {\n      setSyncPlayCurrentPosition();\n      if (enableSync) {\n        await requestSyncPlaySync();\n      }\n    }\n  }\n\n  Future<void> dispose({bool disposeSyncPlayController = true}) async {\n    if (disposeSyncPlayController) {\n      try {\n        syncplayRoom = '';\n        syncplayClientRtt = 0;\n        await syncplayController?.disconnect();\n        syncplayController = null;\n      } catch (_) {}\n    }\n    try {\n      await cancelPlayerDebugInfoSubscription();\n    } catch (_) {}\n    await mediaPlayer?.dispose();\n    mediaPlayer = null;\n    videoController = null;\n  }\n\n  Future<void> stop() async {\n    try {\n      await mediaPlayer?.stop();\n      loading = true;\n    } catch (_) {}\n  }\n\n  Future<Uint8List?> screenshot({String format = 'image/jpeg'}) async {\n    return await mediaPlayer!.screenshot(format: format);\n  }\n\n  void setButtonForwardTime(int time) {\n    buttonSkipTime = time;\n    setting.put(SettingBoxKey.buttonSkipTime, time);\n  }\n\n  void setArrowKeyForwardTime(int time) {\n    arrowKeySkipTime = time;\n    setting.put(SettingBoxKey.arrowKeySkipTime, time);\n  }\n\n  /// 加载弹幕 (离线模式优先从缓存加载，无缓存时尝试在线获取)\n  Future<void> _loadDanmaku(\n      int bangumiId, String pluginName, int episode) async {\n    if (isLocalPlayback) {\n      await _loadCachedDanmaku(bangumiId, pluginName, episode);\n    } else {\n      getDanDanmakuByBgmBangumiID(bangumiId, episode);\n    }\n  }\n\n  Future<void> _loadCachedDanmaku(\n      int bangumiId, String pluginName, int episode) async {\n    if (danmakuLoading) {\n      KazumiLogger()\n          .i('PlayerController: danmaku is loading, ignore duplicate request');\n      return;\n    }\n\n    KazumiLogger().i(\n        'PlayerController: attempting to load cached danmaku for episode $episode');\n    danmakuLoading = true;\n    try {\n      danDanmakus.clear();\n      final downloadController = Modular.get<DownloadController>();\n      final cachedDanmakus = await downloadController.getCachedDanmakus(\n        bangumiId,\n        pluginName,\n        episode,\n      );\n\n      if (cachedDanmakus != null && cachedDanmakus.isNotEmpty) {\n        addDanmakus(cachedDanmakus);\n        KazumiLogger().i(\n            'PlayerController: loaded ${cachedDanmakus.length} cached danmakus');\n      } else {\n        KazumiLogger()\n            .i('PlayerController: no cached danmaku, attempting online fetch');\n        try {\n          bangumiID =\n              await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bangumiId);\n          if (bangumiID != 0) {\n            var res = await DanmakuRequest.getDanDanmaku(bangumiID, episode);\n            if (res.isNotEmpty) {\n              addDanmakus(res);\n              KazumiLogger()\n                  .i('PlayerController: fetched ${res.length} danmakus online');\n              _saveDanmakuToCache(\n                  downloadController, bangumiId, pluginName, episode, res);\n            }\n          }\n        } catch (e) {\n          KazumiLogger().w(\n              'PlayerController: failed to fetch danmaku online (may be offline)',\n              error: e);\n        }\n      }\n    } catch (e) {\n      KazumiLogger()\n          .w('PlayerController: failed to load cached danmaku', error: e);\n    } finally {\n      danmakuLoading = false;\n    }\n  }\n\n  void _saveDanmakuToCache(DownloadController downloadController, int bangumiId,\n      String pluginName, int episode, List<Danmaku> danmakus) {\n    try {\n      downloadController.updateCachedDanmakus(\n        bangumiId,\n        pluginName,\n        episode,\n        danmakus,\n        bangumiID,\n      );\n      KazumiLogger()\n          .i('PlayerController: saved ${danmakus.length} danmakus to cache');\n    } catch (e) {\n      KazumiLogger()\n          .w('PlayerController: failed to save danmaku to cache', error: e);\n    }\n  }\n\n  Future<void> getDanDanmakuByBgmBangumiID(\n      int bgmBangumiID, int episode) async {\n    if (danmakuLoading) {\n      KazumiLogger()\n          .i('PlayerController: danmaku is loading, ignore duplicate request');\n      return;\n    }\n\n    KazumiLogger().i(\n        'PlayerController: attempting to get danmaku [BgmBangumiID] $bgmBangumiID');\n    danmakuLoading = true;\n    try {\n      danDanmakus.clear();\n      bangumiID =\n          await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bgmBangumiID);\n      var res = await DanmakuRequest.getDanDanmaku(bangumiID, episode);\n      addDanmakus(res);\n    } catch (e) {\n      KazumiLogger().w(\n          'PlayerController: failed to get danmaku [BgmBangumiID] $bgmBangumiID',\n          error: e);\n    } finally {\n      danmakuLoading = false;\n    }\n  }\n\n  Future<void> getDanDanmakuByEpisodeID(int episodeID) async {\n    if (danmakuLoading) {\n      KazumiLogger()\n          .i('PlayerController: danmaku is loading, ignore duplicate request');\n      return;\n    }\n\n    KazumiLogger().i('PlayerController: attempting to get danmaku $episodeID');\n    danmakuLoading = true;\n    try {\n      danDanmakus.clear();\n      var res = await DanmakuRequest.getDanDanmakuByEpisodeID(episodeID);\n      addDanmakus(res);\n    } catch (e) {\n      KazumiLogger().w('PlayerController: failed to get danmaku', error: e);\n    } finally {\n      danmakuLoading = false;\n    }\n  }\n\n  void addDanmakus(List<Danmaku> danmakus) {\n    final bool danmakuDeduplicationEnable = setting.get(SettingBoxKey.danmakuDeduplication, defaultValue: false);\n\n    // 如果启用了弹幕去重功能则处理5秒内相邻重复类似的弹幕进行合并\n    final List<Danmaku> listToAdd  = danmakuDeduplicationEnable ? Utils.mergeDuplicateDanmakus(danmakus, timeWindowSeconds: 5) : danmakus;\n\n    for (var element in listToAdd) {\n      var danmakuList =\n          danDanmakus[element.time.toInt()] ?? List.empty(growable: true);\n      danmakuList.add(element);\n      danDanmakus[element.time.toInt()] = danmakuList;\n    }\n  }\n\n  void lanunchExternalPlayer() async {\n    if ((Platform.isAndroid || Platform.isWindows) && referer.isEmpty) {\n      if (await ExternalPlayer.launchURLWithMIME(videoUrl, 'video/mp4')) {\n        KazumiDialog.dismiss();\n        KazumiDialog.showToast(\n          message: '尝试唤起外部播放器',\n        );\n      } else {\n        KazumiDialog.showToast(\n          message: '唤起外部播放器失败',\n        );\n      }\n    } else if (Platform.isMacOS || Platform.isIOS) {\n      if (await ExternalPlayer.launchURLWithReferer(videoUrl, referer)) {\n        KazumiDialog.dismiss();\n        KazumiDialog.showToast(\n          message: '尝试唤起外部播放器',\n        );\n      } else {\n        KazumiDialog.showToast(\n          message: '唤起外部播放器失败',\n        );\n      }\n    } else if (Platform.isLinux && referer.isEmpty) {\n      KazumiDialog.dismiss();\n      if (await canLaunchUrlString(videoUrl)) {\n        launchUrlString(videoUrl);\n        KazumiDialog.showToast(\n          message: '尝试唤起外部播放器',\n        );\n      } else {\n        KazumiDialog.showToast(\n          message: '无法使用外部播放器',\n        );\n      }\n    } else {\n      if (referer.isEmpty) {\n        KazumiDialog.showToast(\n          message: '暂不支持该设备',\n        );\n      } else {\n        KazumiDialog.showToast(\n          message: '暂不支持该规则',\n        );\n      }\n    }\n  }\n\n  Future<void> createSyncPlayRoom(\n      String room,\n      String username,\n      Future<void> Function(int episode, {int currentRoad, int offset})\n          changeEpisode,\n      {bool enableTLS = true}) async {\n    await syncplayController?.disconnect();\n    final String syncPlayEndPoint = setting.get(SettingBoxKey.syncPlayEndPoint,\n        defaultValue: defaultSyncPlayEndPoint);\n    String syncPlayEndPointHost = '';\n    int syncPlayEndPointPort = 0;\n    KazumiLogger().i('SyncPlay: connecting to $syncPlayEndPoint');\n    try {\n      final parsed = parseSyncPlayEndPoint(syncPlayEndPoint);\n      if (parsed != null) {\n        syncPlayEndPointHost = parsed.host;\n        syncPlayEndPointPort = parsed.port;\n      }\n    } catch (_) {}\n    if (syncPlayEndPointHost == '' || syncPlayEndPointPort == 0) {\n      KazumiDialog.showToast(\n        message: 'SyncPlay: 服务器地址不合法 $syncPlayEndPoint',\n      );\n      KazumiLogger().e('SyncPlay: invalid server address $syncPlayEndPoint');\n      return;\n    }\n    syncplayController =\n        SyncplayClient(host: syncPlayEndPointHost, port: syncPlayEndPointPort);\n    try {\n      await syncplayController!.connect(enableTLS: enableTLS);\n      KazumiLogger().i(\n          'SyncPlay: connected to $syncPlayEndPointHost:$syncPlayEndPointPort');\n      syncplayController!.onGeneralMessage.listen(\n        (message) {\n          // print('SyncPlay: general message: ${message.toString()}');\n        },\n        onError: (error) {\n          print('SyncPlay: error: ${error.message}');\n          if (error is SyncplayConnectionException) {\n            exitSyncPlayRoom();\n            KazumiDialog.showToast(\n              message: 'SyncPlay: 同步中断 ${error.message}',\n              duration: const Duration(seconds: 5),\n              showActionButton: true,\n              actionLabel: '重新连接',\n              onActionPressed: () =>\n                  createSyncPlayRoom(room, username, changeEpisode),\n            );\n          }\n        },\n      );\n      syncplayController!.onRoomMessage.listen(\n        (message) {\n          if (message['type'] == 'init') {\n            if (message['username'] == '') {\n              KazumiDialog.showToast(\n                  message: 'SyncPlay: 您是当前房间中的唯一用户',\n                  duration: const Duration(seconds: 5));\n              setSyncPlayPlayingBangumi();\n            } else {\n              KazumiDialog.showToast(\n                  message:\n                      'SyncPlay: 您不是当前房间中的唯一用户, 当前以用户 ${message['username']} 进度为准');\n            }\n          }\n          if (message['type'] == 'left') {\n            KazumiDialog.showToast(\n                message: 'SyncPlay: ${message['username']} 离开了房间',\n                duration: const Duration(seconds: 5));\n          }\n          if (message['type'] == 'joined') {\n            KazumiDialog.showToast(\n                message: 'SyncPlay: ${message['username']} 加入了房间',\n                duration: const Duration(seconds: 5));\n          }\n        },\n      );\n      syncplayController!.onFileChangedMessage.listen(\n        (message) {\n          print(\n              'SyncPlay: file changed by ${message['setBy']}: ${message['name']}');\n          RegExp regExp = RegExp(r'(\\d+)\\[(\\d+)\\]');\n          Match? match = regExp.firstMatch(message['name']);\n          if (match != null) {\n            int bangumiID = int.tryParse(match.group(1) ?? '0') ?? 0;\n            int episode = int.tryParse(match.group(2) ?? '0') ?? 0;\n            if (bangumiID != 0 && episode != 0 && episode != currentEpisode) {\n              KazumiDialog.showToast(\n                  message:\n                      'SyncPlay: ${message['setBy'] ?? 'unknown'} 切换到第 $episode 话',\n                  duration: const Duration(seconds: 3));\n              changeEpisode(episode, currentRoad: currentRoad);\n            }\n          }\n        },\n      );\n      syncplayController!.onChatMessage.listen(\n        (message) {\n          final String sender = (message['username'] ?? '').toString();\n          final String text = (message['message'] ?? '').toString();\n          final bool fromRemote = message['username'] != username;\n\n          // 将消息转发到流\n          if (!syncPlayChatStreamController.isClosed) {\n            syncPlayChatStreamController.add(SyncPlayChatMessage(\n              username: sender,\n              message: text,\n              fromRemote: fromRemote,\n            ));\n          }\n        },\n        onError: (error) {\n          print('SyncPlay: error: ${error.message}');\n        },\n      );\n      syncplayController!.onPositionChangedMessage.listen(\n        (message) {\n          syncplayClientRtt = (message['clientRtt'].toDouble() * 1000).toInt();\n          print(\n              'SyncPlay: position changed by ${message['setBy']}: [${DateTime.now().millisecondsSinceEpoch / 1000.0}] calculatedPosition ${message['calculatedPositon']} position: ${message['position']} doSeek: ${message['doSeek']} paused: ${message['paused']} clientRtt: ${message['clientRtt']} serverRtt: ${message['serverRtt']} fd: ${message['fd']}');\n          if (message['paused'] != !playing) {\n            if (message['paused']) {\n              if (message['position'] != 0) {\n                KazumiDialog.showToast(\n                    message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 暂停了播放',\n                    duration: const Duration(seconds: 3));\n                pause(enableSync: false);\n              }\n            } else {\n              if (message['position'] != 0) {\n                KazumiDialog.showToast(\n                    message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 开始了播放',\n                    duration: const Duration(seconds: 3));\n                play(enableSync: false);\n              }\n            }\n          }\n          if ((((playerPosition.inMilliseconds -\n                              (message['calculatedPositon'].toDouble() * 1000)\n                                  .toInt())\n                          .abs() >\n                      1000) ||\n                  message['doSeek']) &&\n              duration.inMilliseconds > 0) {\n            seek(\n                Duration(\n                    milliseconds:\n                        (message['calculatedPositon'].toDouble() * 1000)\n                            .toInt()),\n                enableSync: false);\n          }\n        },\n      );\n      await syncplayController!.joinRoom(room, username);\n      syncplayRoom = room;\n    } catch (e) {\n      print('SyncPlay: error: $e');\n    }\n  }\n\n  void setSyncPlayCurrentPosition(\n      {bool? forceSyncPlaying, double? forceSyncPosition}) {\n    if (syncplayController == null) {\n      return;\n    }\n    forceSyncPlaying ??= playing;\n    syncplayController!.setPaused(!forceSyncPlaying);\n    syncplayController!.setPosition((forceSyncPosition ??\n        (((currentPosition.inMilliseconds - playerPosition.inMilliseconds)\n                    .abs() >\n                2000)\n            ? currentPosition.inMilliseconds.toDouble() / 1000\n            : playerPosition.inMilliseconds.toDouble() / 1000)));\n  }\n\n  Future<void> setSyncPlayPlayingBangumi(\n      {bool? forceSyncPlaying, double? forceSyncPosition}) async {\n    await syncplayController!\n        .setSyncPlayPlaying(\"$bangumiId[$currentEpisode]\", 10800, 220514438);\n    setSyncPlayCurrentPosition(\n        forceSyncPlaying: forceSyncPlaying,\n        forceSyncPosition: forceSyncPosition);\n    await requestSyncPlaySync();\n  }\n\n  Future<void> requestSyncPlaySync({bool? doSeek}) async {\n    await syncplayController!.sendSyncPlaySyncRequest(doSeek: doSeek);\n  }\n\n  Future<void> sendSyncPlayChatMessage(String message) async {\n    if (syncplayController == null) {\n      return;\n    }\n    await syncplayController!.sendChatMessage(message);\n  }\n\n  Future<void> exitSyncPlayRoom() async {\n    if (syncplayController == null) {\n      return;\n    }\n    await syncplayController!.disconnect();\n    syncplayController = null;\n    syncplayRoom = '';\n    syncplayClientRtt = 0;\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/player_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'player_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$PlayerController on _PlayerController, Store {\n  late final _$danDanmakusAtom =\n      Atom(name: '_PlayerController.danDanmakus', context: context);\n\n  @override\n  Map<int, List<Danmaku>> get danDanmakus {\n    _$danDanmakusAtom.reportRead();\n    return super.danDanmakus;\n  }\n\n  @override\n  set danDanmakus(Map<int, List<Danmaku>> value) {\n    _$danDanmakusAtom.reportWrite(value, super.danDanmakus, () {\n      super.danDanmakus = value;\n    });\n  }\n\n  late final _$danmakuOnAtom =\n      Atom(name: '_PlayerController.danmakuOn', context: context);\n\n  @override\n  bool get danmakuOn {\n    _$danmakuOnAtom.reportRead();\n    return super.danmakuOn;\n  }\n\n  @override\n  set danmakuOn(bool value) {\n    _$danmakuOnAtom.reportWrite(value, super.danmakuOn, () {\n      super.danmakuOn = value;\n    });\n  }\n\n  late final _$danmakuLoadingAtom =\n      Atom(name: '_PlayerController.danmakuLoading', context: context);\n\n  @override\n  bool get danmakuLoading {\n    _$danmakuLoadingAtom.reportRead();\n    return super.danmakuLoading;\n  }\n\n  @override\n  set danmakuLoading(bool value) {\n    _$danmakuLoadingAtom.reportWrite(value, super.danmakuLoading, () {\n      super.danmakuLoading = value;\n    });\n  }\n\n  late final _$syncplayRoomAtom =\n      Atom(name: '_PlayerController.syncplayRoom', context: context);\n\n  @override\n  String get syncplayRoom {\n    _$syncplayRoomAtom.reportRead();\n    return super.syncplayRoom;\n  }\n\n  @override\n  set syncplayRoom(String value) {\n    _$syncplayRoomAtom.reportWrite(value, super.syncplayRoom, () {\n      super.syncplayRoom = value;\n    });\n  }\n\n  late final _$syncplayClientRttAtom =\n      Atom(name: '_PlayerController.syncplayClientRtt', context: context);\n\n  @override\n  int get syncplayClientRtt {\n    _$syncplayClientRttAtom.reportRead();\n    return super.syncplayClientRtt;\n  }\n\n  @override\n  set syncplayClientRtt(int value) {\n    _$syncplayClientRttAtom.reportWrite(value, super.syncplayClientRtt, () {\n      super.syncplayClientRtt = value;\n    });\n  }\n\n  late final _$aspectRatioTypeAtom =\n      Atom(name: '_PlayerController.aspectRatioType', context: context);\n\n  @override\n  int get aspectRatioType {\n    _$aspectRatioTypeAtom.reportRead();\n    return super.aspectRatioType;\n  }\n\n  @override\n  set aspectRatioType(int value) {\n    _$aspectRatioTypeAtom.reportWrite(value, super.aspectRatioType, () {\n      super.aspectRatioType = value;\n    });\n  }\n\n  late final _$superResolutionTypeAtom =\n      Atom(name: '_PlayerController.superResolutionType', context: context);\n\n  @override\n  int get superResolutionType {\n    _$superResolutionTypeAtom.reportRead();\n    return super.superResolutionType;\n  }\n\n  @override\n  set superResolutionType(int value) {\n    _$superResolutionTypeAtom.reportWrite(value, super.superResolutionType, () {\n      super.superResolutionType = value;\n    });\n  }\n\n  late final _$volumeAtom =\n      Atom(name: '_PlayerController.volume', context: context);\n\n  @override\n  double get volume {\n    _$volumeAtom.reportRead();\n    return super.volume;\n  }\n\n  @override\n  set volume(double value) {\n    _$volumeAtom.reportWrite(value, super.volume, () {\n      super.volume = value;\n    });\n  }\n\n  late final _$brightnessAtom =\n      Atom(name: '_PlayerController.brightness', context: context);\n\n  @override\n  double get brightness {\n    _$brightnessAtom.reportRead();\n    return super.brightness;\n  }\n\n  @override\n  set brightness(double value) {\n    _$brightnessAtom.reportWrite(value, super.brightness, () {\n      super.brightness = value;\n    });\n  }\n\n  late final _$lockPanelAtom =\n      Atom(name: '_PlayerController.lockPanel', context: context);\n\n  @override\n  bool get lockPanel {\n    _$lockPanelAtom.reportRead();\n    return super.lockPanel;\n  }\n\n  @override\n  set lockPanel(bool value) {\n    _$lockPanelAtom.reportWrite(value, super.lockPanel, () {\n      super.lockPanel = value;\n    });\n  }\n\n  late final _$showVideoControllerAtom =\n      Atom(name: '_PlayerController.showVideoController', context: context);\n\n  @override\n  bool get showVideoController {\n    _$showVideoControllerAtom.reportRead();\n    return super.showVideoController;\n  }\n\n  @override\n  set showVideoController(bool value) {\n    _$showVideoControllerAtom.reportWrite(value, super.showVideoController, () {\n      super.showVideoController = value;\n    });\n  }\n\n  late final _$showSeekTimeAtom =\n      Atom(name: '_PlayerController.showSeekTime', context: context);\n\n  @override\n  bool get showSeekTime {\n    _$showSeekTimeAtom.reportRead();\n    return super.showSeekTime;\n  }\n\n  @override\n  set showSeekTime(bool value) {\n    _$showSeekTimeAtom.reportWrite(value, super.showSeekTime, () {\n      super.showSeekTime = value;\n    });\n  }\n\n  late final _$showBrightnessAtom =\n      Atom(name: '_PlayerController.showBrightness', context: context);\n\n  @override\n  bool get showBrightness {\n    _$showBrightnessAtom.reportRead();\n    return super.showBrightness;\n  }\n\n  @override\n  set showBrightness(bool value) {\n    _$showBrightnessAtom.reportWrite(value, super.showBrightness, () {\n      super.showBrightness = value;\n    });\n  }\n\n  late final _$showVolumeAtom =\n      Atom(name: '_PlayerController.showVolume', context: context);\n\n  @override\n  bool get showVolume {\n    _$showVolumeAtom.reportRead();\n    return super.showVolume;\n  }\n\n  @override\n  set showVolume(bool value) {\n    _$showVolumeAtom.reportWrite(value, super.showVolume, () {\n      super.showVolume = value;\n    });\n  }\n\n  late final _$showPlaySpeedAtom =\n      Atom(name: '_PlayerController.showPlaySpeed', context: context);\n\n  @override\n  bool get showPlaySpeed {\n    _$showPlaySpeedAtom.reportRead();\n    return super.showPlaySpeed;\n  }\n\n  @override\n  set showPlaySpeed(bool value) {\n    _$showPlaySpeedAtom.reportWrite(value, super.showPlaySpeed, () {\n      super.showPlaySpeed = value;\n    });\n  }\n\n  late final _$brightnessSeekingAtom =\n      Atom(name: '_PlayerController.brightnessSeeking', context: context);\n\n  @override\n  bool get brightnessSeeking {\n    _$brightnessSeekingAtom.reportRead();\n    return super.brightnessSeeking;\n  }\n\n  @override\n  set brightnessSeeking(bool value) {\n    _$brightnessSeekingAtom.reportWrite(value, super.brightnessSeeking, () {\n      super.brightnessSeeking = value;\n    });\n  }\n\n  late final _$volumeSeekingAtom =\n      Atom(name: '_PlayerController.volumeSeeking', context: context);\n\n  @override\n  bool get volumeSeeking {\n    _$volumeSeekingAtom.reportRead();\n    return super.volumeSeeking;\n  }\n\n  @override\n  set volumeSeeking(bool value) {\n    _$volumeSeekingAtom.reportWrite(value, super.volumeSeeking, () {\n      super.volumeSeeking = value;\n    });\n  }\n\n  late final _$canHidePlayerPanelAtom =\n      Atom(name: '_PlayerController.canHidePlayerPanel', context: context);\n\n  @override\n  bool get canHidePlayerPanel {\n    _$canHidePlayerPanelAtom.reportRead();\n    return super.canHidePlayerPanel;\n  }\n\n  @override\n  set canHidePlayerPanel(bool value) {\n    _$canHidePlayerPanelAtom.reportWrite(value, super.canHidePlayerPanel, () {\n      super.canHidePlayerPanel = value;\n    });\n  }\n\n  late final _$loadingAtom =\n      Atom(name: '_PlayerController.loading', context: context);\n\n  @override\n  bool get loading {\n    _$loadingAtom.reportRead();\n    return super.loading;\n  }\n\n  @override\n  set loading(bool value) {\n    _$loadingAtom.reportWrite(value, super.loading, () {\n      super.loading = value;\n    });\n  }\n\n  late final _$playingAtom =\n      Atom(name: '_PlayerController.playing', context: context);\n\n  @override\n  bool get playing {\n    _$playingAtom.reportRead();\n    return super.playing;\n  }\n\n  @override\n  set playing(bool value) {\n    _$playingAtom.reportWrite(value, super.playing, () {\n      super.playing = value;\n    });\n  }\n\n  late final _$isBufferingAtom =\n      Atom(name: '_PlayerController.isBuffering', context: context);\n\n  @override\n  bool get isBuffering {\n    _$isBufferingAtom.reportRead();\n    return super.isBuffering;\n  }\n\n  @override\n  set isBuffering(bool value) {\n    _$isBufferingAtom.reportWrite(value, super.isBuffering, () {\n      super.isBuffering = value;\n    });\n  }\n\n  late final _$completedAtom =\n      Atom(name: '_PlayerController.completed', context: context);\n\n  @override\n  bool get completed {\n    _$completedAtom.reportRead();\n    return super.completed;\n  }\n\n  @override\n  set completed(bool value) {\n    _$completedAtom.reportWrite(value, super.completed, () {\n      super.completed = value;\n    });\n  }\n\n  late final _$currentPositionAtom =\n      Atom(name: '_PlayerController.currentPosition', context: context);\n\n  @override\n  Duration get currentPosition {\n    _$currentPositionAtom.reportRead();\n    return super.currentPosition;\n  }\n\n  @override\n  set currentPosition(Duration value) {\n    _$currentPositionAtom.reportWrite(value, super.currentPosition, () {\n      super.currentPosition = value;\n    });\n  }\n\n  late final _$bufferAtom =\n      Atom(name: '_PlayerController.buffer', context: context);\n\n  @override\n  Duration get buffer {\n    _$bufferAtom.reportRead();\n    return super.buffer;\n  }\n\n  @override\n  set buffer(Duration value) {\n    _$bufferAtom.reportWrite(value, super.buffer, () {\n      super.buffer = value;\n    });\n  }\n\n  late final _$durationAtom =\n      Atom(name: '_PlayerController.duration', context: context);\n\n  @override\n  Duration get duration {\n    _$durationAtom.reportRead();\n    return super.duration;\n  }\n\n  @override\n  set duration(Duration value) {\n    _$durationAtom.reportWrite(value, super.duration, () {\n      super.duration = value;\n    });\n  }\n\n  late final _$playerSpeedAtom =\n      Atom(name: '_PlayerController.playerSpeed', context: context);\n\n  @override\n  double get playerSpeed {\n    _$playerSpeedAtom.reportRead();\n    return super.playerSpeed;\n  }\n\n  @override\n  set playerSpeed(double value) {\n    _$playerSpeedAtom.reportWrite(value, super.playerSpeed, () {\n      super.playerSpeed = value;\n    });\n  }\n\n  late final _$playerLogAtom =\n      Atom(name: '_PlayerController.playerLog', context: context);\n\n  @override\n  ObservableList<String> get playerLog {\n    _$playerLogAtom.reportRead();\n    return super.playerLog;\n  }\n\n  @override\n  set playerLog(ObservableList<String> value) {\n    _$playerLogAtom.reportWrite(value, super.playerLog, () {\n      super.playerLog = value;\n    });\n  }\n\n  late final _$playerWidthAtom =\n      Atom(name: '_PlayerController.playerWidth', context: context);\n\n  @override\n  int get playerWidth {\n    _$playerWidthAtom.reportRead();\n    return super.playerWidth;\n  }\n\n  @override\n  set playerWidth(int value) {\n    _$playerWidthAtom.reportWrite(value, super.playerWidth, () {\n      super.playerWidth = value;\n    });\n  }\n\n  late final _$playerHeightAtom =\n      Atom(name: '_PlayerController.playerHeight', context: context);\n\n  @override\n  int get playerHeight {\n    _$playerHeightAtom.reportRead();\n    return super.playerHeight;\n  }\n\n  @override\n  set playerHeight(int value) {\n    _$playerHeightAtom.reportWrite(value, super.playerHeight, () {\n      super.playerHeight = value;\n    });\n  }\n\n  late final _$playerVideoParamsAtom =\n      Atom(name: '_PlayerController.playerVideoParams', context: context);\n\n  @override\n  String get playerVideoParams {\n    _$playerVideoParamsAtom.reportRead();\n    return super.playerVideoParams;\n  }\n\n  @override\n  set playerVideoParams(String value) {\n    _$playerVideoParamsAtom.reportWrite(value, super.playerVideoParams, () {\n      super.playerVideoParams = value;\n    });\n  }\n\n  late final _$playerAudioParamsAtom =\n      Atom(name: '_PlayerController.playerAudioParams', context: context);\n\n  @override\n  String get playerAudioParams {\n    _$playerAudioParamsAtom.reportRead();\n    return super.playerAudioParams;\n  }\n\n  @override\n  set playerAudioParams(String value) {\n    _$playerAudioParamsAtom.reportWrite(value, super.playerAudioParams, () {\n      super.playerAudioParams = value;\n    });\n  }\n\n  late final _$playerPlaylistAtom =\n      Atom(name: '_PlayerController.playerPlaylist', context: context);\n\n  @override\n  String get playerPlaylist {\n    _$playerPlaylistAtom.reportRead();\n    return super.playerPlaylist;\n  }\n\n  @override\n  set playerPlaylist(String value) {\n    _$playerPlaylistAtom.reportWrite(value, super.playerPlaylist, () {\n      super.playerPlaylist = value;\n    });\n  }\n\n  late final _$playerAudioTracksAtom =\n      Atom(name: '_PlayerController.playerAudioTracks', context: context);\n\n  @override\n  String get playerAudioTracks {\n    _$playerAudioTracksAtom.reportRead();\n    return super.playerAudioTracks;\n  }\n\n  @override\n  set playerAudioTracks(String value) {\n    _$playerAudioTracksAtom.reportWrite(value, super.playerAudioTracks, () {\n      super.playerAudioTracks = value;\n    });\n  }\n\n  late final _$playerVideoTracksAtom =\n      Atom(name: '_PlayerController.playerVideoTracks', context: context);\n\n  @override\n  String get playerVideoTracks {\n    _$playerVideoTracksAtom.reportRead();\n    return super.playerVideoTracks;\n  }\n\n  @override\n  set playerVideoTracks(String value) {\n    _$playerVideoTracksAtom.reportWrite(value, super.playerVideoTracks, () {\n      super.playerVideoTracks = value;\n    });\n  }\n\n  late final _$playerAudioBitrateAtom =\n      Atom(name: '_PlayerController.playerAudioBitrate', context: context);\n\n  @override\n  String get playerAudioBitrate {\n    _$playerAudioBitrateAtom.reportRead();\n    return super.playerAudioBitrate;\n  }\n\n  @override\n  set playerAudioBitrate(String value) {\n    _$playerAudioBitrateAtom.reportWrite(value, super.playerAudioBitrate, () {\n      super.playerAudioBitrate = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\ndanDanmakus: ${danDanmakus},\ndanmakuOn: ${danmakuOn},\ndanmakuLoading: ${danmakuLoading},\nsyncplayRoom: ${syncplayRoom},\nsyncplayClientRtt: ${syncplayClientRtt},\naspectRatioType: ${aspectRatioType},\nsuperResolutionType: ${superResolutionType},\nvolume: ${volume},\nbrightness: ${brightness},\nlockPanel: ${lockPanel},\nshowVideoController: ${showVideoController},\nshowSeekTime: ${showSeekTime},\nshowBrightness: ${showBrightness},\nshowVolume: ${showVolume},\nshowPlaySpeed: ${showPlaySpeed},\nbrightnessSeeking: ${brightnessSeeking},\nvolumeSeeking: ${volumeSeeking},\ncanHidePlayerPanel: ${canHidePlayerPanel},\nloading: ${loading},\nplaying: ${playing},\nisBuffering: ${isBuffering},\ncompleted: ${completed},\ncurrentPosition: ${currentPosition},\nbuffer: ${buffer},\nduration: ${duration},\nplayerSpeed: ${playerSpeed},\nplayerLog: ${playerLog},\nplayerWidth: ${playerWidth},\nplayerHeight: ${playerHeight},\nplayerVideoParams: ${playerVideoParams},\nplayerAudioParams: ${playerAudioParams},\nplayerPlaylist: ${playerPlaylist},\nplayerAudioTracks: ${playerAudioTracks},\nplayerVideoTracks: ${playerVideoTracks},\nplayerAudioBitrate: ${playerAudioBitrate}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/player_item.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'package:audio_video_progress_bar/audio_video_progress_bar.dart';\nimport 'package:kazumi/pages/player/player_item_panel.dart';\nimport 'package:kazumi/pages/player/smallest_player_item_panel.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/webdav.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:canvas_danmaku/canvas_danmaku.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';\nimport 'package:flutter_volume_controller/flutter_volume_controller.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/pages/collect/collect_controller.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/request/damaku.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_search_response.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_episode_response.dart';\nimport 'package:kazumi/pages/player/player_item_surface.dart';\nimport 'package:mobx/mobx.dart' as mobx;\nimport 'package:kazumi/pages/my/my_controller.dart';\nimport 'package:saver_gallery/saver_gallery.dart';\n\nclass PlayerItem extends StatefulWidget {\n  const PlayerItem({\n    super.key,\n    required this.openMenu,\n    required this.locateEpisode,\n    required this.changeEpisode,\n    required this.onBackPressed,\n    required this.keyboardFocus,\n    required this.sendDanmaku,\n    required this.showDanmakuDestinationPickerAndSend,\n    required this.pauseForTimedShutdown,\n    this.disableAnimations = false,\n  });\n\n  final VoidCallback openMenu;\n  final VoidCallback locateEpisode;\n  final Future<void> Function(int episode, {int currentRoad, int offset})\n      changeEpisode;\n  final void Function(BuildContext) onBackPressed;\n  final void Function(String) sendDanmaku;\n  final FocusNode keyboardFocus;\n  final bool disableAnimations;\n  final void Function(String) showDanmakuDestinationPickerAndSend;\n  final VoidCallback pauseForTimedShutdown;\n\n  @override\n  State<PlayerItem> createState() => _PlayerItemState();\n}\n\nclass _PlayerItemState extends State<PlayerItem>\n    with\n        WindowListener,\n        WidgetsBindingObserver,\n        SingleTickerProviderStateMixin {\n  Box setting = GStorage.setting;\n  final PlayerController playerController = Modular.get<PlayerController>();\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final HistoryController historyController = Modular.get<HistoryController>();\n  final CollectController collectController = Modular.get<CollectController>();\n  final MyController myController = Modular.get<MyController>();\n  late Map<String, List<String>> keyboardShortcuts;\n  late List<String> keyboardActionsNeedLongPress;\n  late Map<String, void Function()> keyboardActions;\n\n  // 1. 在看\n  // 2. 想看\n  // 3. 搁置\n  // 4. 看过\n  // 5. 抛弃\n  late int collectType;\n  late bool webDavEnable;\n  late bool webDavEnableHistory;\n\n  // 弹幕\n  final _danmuKey = GlobalKey();\n  late bool _border;\n  late double _opacity;\n  late double _fontSize;\n  late double _danmakuArea;\n  late bool _hideTop;\n  late bool _hideBottom;\n  late bool _hideScroll;\n  late bool _massiveMode;\n  late bool _danmakuColor;\n  late bool _danmakuBiliBiliSource;\n  late bool _danmakuGamerSource;\n  late bool _danmakuDanDanSource;\n  late double _danmakuDuration;\n  late double _danmakuLineHeight;\n  late int _danmakuFontWeight;\n  late bool _danmakuUseSystemFont;\n  late double _danmakuBorderSize;\n\n  // 硬件解码\n  late bool haEnable;\n  late bool autoPlayNext;\n\n  Timer? hideTimer;\n  Timer? playerTimer;\n  Timer? mouseScrollerTimer;\n  Timer? hideVolumeUITimer;\n\n  double lastVolume = 0;\n\n  // 过渡动画控制器\n  AnimationController? animationController;\n\n  double lastPlayerSpeed = 1.0;\n  int episodeNum = 0;\n\n  late mobx.ReactionDisposer _fullscreenListener;\n\n  /// 处理 Android/iOS 应用后台或熄屏\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    super.didChangeAppLifecycleState(state);\n    try {\n      if (playerController.playerPlaying) {\n        playerController.danmakuController.resume();\n      }\n    } catch (_) {}\n  }\n\n  void _loadShortcuts() {\n    keyboardShortcuts = {};\n    defaultShortcuts.forEach((key, defaultValue) {\n      keyboardShortcuts[key] = setting\n          .get('shortcut_$key', defaultValue: defaultValue)\n          .cast<String>();\n    });\n  }\n\n  void _initKeyboardActions() {\n    //需要实现长按的功能列表。\n    keyboardActionsNeedLongPress = [\"forward\"];\n    //快捷键功能对应表\n    keyboardActions = {\n      'playorpause': () => playerController.playOrPause(),\n      'forward': () async => handleShortcutForwardDown(),\n      'rewind': () async => handleShortcutRewind(),\n      'next': () async => handlePreNextEpisode('next'),\n      'prev': () async => handlePreNextEpisode('prev'),\n      'volumeup': () async => handleShortcutVolumeChange('up'),\n      'volumedown': () async => handleShortcutVolumeChange('down'),\n      'togglemute': () async => handleShortcutVolumeChange('mute'),\n      'fullscreen': () => handleShortcutFullscreen(),\n      'screenshot': () async => handleScreenshot(),\n      'skip': () async => skipOP(),\n      'exitfullscreen': () => handleShortcutExitFullscreen(),\n      'toggledanmaku': () => handleDanmaku(),\n      'speed1': () async => setPlaybackSpeed(1.0),\n      'speed2': () async => setPlaybackSpeed(2.0),\n      'speed3': () async => setPlaybackSpeed(3.0),\n      'speedup': () async => handleSpeedChange('up'),\n      'speeddown': () async => handleSpeedChange('down'),\n      // 开始对应长按功能\n      // 如需对应长按功能，例如对功能'func'对应长按，请分别添加'funcRepeat'和'funcUp'。\n      'forwardRepeat': () async => handleShortcutForwardRepeat(),\n      'forwardUp': () async => handleShortcutForwardUp(),\n    };\n  }\n\n  //初始化播放器菜单\n  void _initPlayerMenu() {\n    Utils.initPlayerMenu(keyboardActions);\n  }\n\n  //销毁播放器菜单\n  void _disposePlayerMenu() {\n    Utils.disposePlayerMenu();\n  }\n\n  //快捷键按下\n  bool handleShortcutDown(String keyLabel) {\n    for (final entry in keyboardShortcuts.entries) {\n      final func = entry.key;\n      final keys = entry.value;\n      if (keys.contains(keyLabel)) {\n        final action = keyboardActions[func];\n        if (action != null) {\n          action();\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  // 快捷键长按\n  bool handleShortcutLongPress(String keyLabel, String mode) {\n    for (final func in keyboardActionsNeedLongPress) {\n      final keys = keyboardShortcuts[func];\n      if (keys?.contains(keyLabel) == true) {\n        final action = keyboardActions[func + mode];\n        if (action != null) {\n          action();\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  //上一集下一集动作\n  Future<void> handlePreNextEpisode(String direction) async {\n    if (videoPageController.loading) return;\n    final currentRoad = videoPageController.currentRoad;\n    final episodes = videoPageController.roadList[currentRoad].data;\n    int targetEpisode;\n    if (direction == 'next') {\n      targetEpisode = videoPageController.currentEpisode + 1;\n    } else if (direction == 'prev') {\n      targetEpisode = videoPageController.currentEpisode - 1;\n    } else {\n      return;\n    }\n\n    if (targetEpisode > episodes.length) {\n      KazumiDialog.showToast(message: '已经是最新一集');\n      return;\n    }\n    if (targetEpisode <= 0) {\n      KazumiDialog.showToast(message: '已经是第一集');\n      return;\n    }\n\n    final identifier =\n        videoPageController.roadList[currentRoad].identifier[targetEpisode - 1];\n    KazumiDialog.showToast(message: '正在加载$identifier');\n    widget.changeEpisode(targetEpisode, currentRoad: currentRoad);\n  }\n\n  //快退快捷键动作\n  Future<void> handleShortcutRewind() async {\n    int skipTime = playerController.arrowKeySkipTime;\n    int current = playerController.currentPosition.inSeconds;\n    int targetPosition;\n\n    targetPosition = current - skipTime;\n    if (targetPosition < 0) targetPosition = 0;\n\n    try {\n      playerTimer?.cancel();\n      await playerController.seek(Duration(seconds: targetPosition));\n      playerTimer = getPlayerTimer();\n    } catch (e) {\n      KazumiLogger().e('PlayerController: seek failed', error: e);\n    }\n  }\n\n  // 快进快捷键动作\n  Future<void> handleShortcutForwardDown() async {\n    lastPlayerSpeed = playerController.playerSpeed;\n  }\n\n  Future<void> handleShortcutForwardRepeat() async {\n    final double defaultShortcutForwardPlaySpeed = setting.get(SettingBoxKey.defaultShortcutForwardPlaySpeed, defaultValue: 2.0);\n    if (playerController.playerSpeed < defaultShortcutForwardPlaySpeed) {\n      playerController.showPlaySpeed = true;\n      setPlaybackSpeed(defaultShortcutForwardPlaySpeed);\n    }\n  }\n\n  Future<void> handleShortcutForwardUp() async {\n    int skipTime = playerController.arrowKeySkipTime;\n    int current = playerController.currentPosition.inSeconds;\n    int total = playerController.duration.inSeconds;\n    int targetPosition;\n\n    targetPosition = current + skipTime;\n    if (targetPosition > total) targetPosition = total;\n    if (playerController.showPlaySpeed) {\n      playerController.showPlaySpeed = false;\n      setPlaybackSpeed(lastPlayerSpeed);\n    } else {\n      try {\n        playerTimer?.cancel();\n        playerController.seek(Duration(seconds: targetPosition));\n        playerTimer = getPlayerTimer();\n      } catch (e) {\n        KazumiLogger().e('PlayerController: seek failed', error: e);\n      }\n    }\n  }\n\n  //全屏快捷键动作\n  void handleShortcutFullscreen() {\n    if (!videoPageController.isPip) handleFullscreen();\n  }\n\n  //退出全屏快捷键动作\n  void handleShortcutExitFullscreen() {\n    if (videoPageController.isFullscreen && !Utils.isTablet()) {\n      try {\n        playerController.danmakuController.clear();\n      } catch (_) {}\n      Utils.exitFullScreen();\n      videoPageController.isFullscreen = !videoPageController.isFullscreen;\n    } else if (!Platform.isMacOS) {\n      playerController.pause();\n      windowManager.hide();\n    }\n  }\n\n  void _handleTap() {\n    if (Utils.isDesktop()) {\n      playerController.playOrPause();\n    } else {\n      if (playerController.showVideoController) {\n        hideVideoController();\n      } else {\n        displayVideoController();\n      }\n    }\n  }\n\n  void _handleDoubleTap() {\n    if (Utils.isDesktop() && !videoPageController.isPip) {\n      handleFullscreen();\n    } else {\n      playerController.playOrPause();\n    }\n  }\n\n  void _handleHove() {\n    if (!playerController.showVideoController) {\n      displayVideoController();\n    }\n    hideTimer?.cancel();\n    startHideTimer();\n  }\n\n  void _handleMouseScroller() {\n    playerController.showVolume = true;\n    mouseScrollerTimer?.cancel();\n    mouseScrollerTimer = Timer(const Duration(seconds: 2), () {\n      if (mounted) {\n        playerController.showVolume = false;\n      }\n      mouseScrollerTimer = null;\n    });\n  }\n\n  //跳过指定秒数\n  Future<void> skipOP() async {\n    await playerController.seek(playerController.currentPosition +\n        Duration(seconds: playerController.buttonSkipTime));\n  }\n\n  void handleDanmaku() {\n    playerController.danmakuController.clear();\n    // if true, turn off danmaku.\n    if (playerController.danmakuOn) {\n      setState(() {\n        playerController.danmakuOn = false;\n      });\n      setting.put(SettingBoxKey.danmakuEnabledByDefault, false);\n      return;\n    }\n    // if false and empty, show dialog.\n    if (playerController.danDanmakus.isEmpty) {\n      showDanmakuSwitch();\n      return;\n    }\n    // turn on danmaku.\n    setState(() {\n      playerController.danmakuOn = true;\n    });\n    setting.put(SettingBoxKey.danmakuEnabledByDefault, true);\n  }\n\n  Future<void> _uploadHistoryToWebDav() async {\n    if (webDavEnable && webDavEnableHistory) {\n      try {\n        var webDav = WebDav();\n        await webDav.updateHistory();\n      } catch (_) {}\n    }\n  }\n\n  void _handleFullscreenChange(BuildContext context) async {\n    playerController.lockPanel = false;\n    playerController.danmakuController.clear();\n\n    await _uploadHistoryToWebDav();\n  }\n\n  void handleProgressBarDragStart(ThumbDragDetails details) {\n    playerTimer?.cancel();\n    playerController.pause(enableSync: false);\n    hideTimer?.cancel();\n    playerController.showVideoController = true;\n  }\n\n  void handleProgressBarDragEnd() {\n    playerController.play(enableSync: false);\n    startHideTimer();\n    playerTimer?.cancel();\n    playerTimer = getPlayerTimer();\n  }\n\n  //截图\n  Future<void> handleScreenshot() async {\n    KazumiDialog.showToast(message: '截图中...');\n    try {\n      Uint8List? screenshot =\n          await playerController.screenshot(format: 'image/png');\n\n      if (screenshot == null) {\n        KazumiDialog.showToast(message: '截图失败：未获取到图像');\n        return;\n      }\n\n      if (Utils.isDesktop()) {\n        KazumiDialog.showToast(message: '桌面端暂未支持保存截图');\n        return;\n      }\n      final result = await SaverGallery.saveImage(\n        screenshot,\n        fileName: DateTime.timestamp().millisecondsSinceEpoch.toString(),\n        skipIfExists: false,\n      );\n      if (result.isSuccess) {\n        KazumiDialog.showToast(message: '截图保存到相簿成功');\n      } else {\n        KazumiDialog.showToast(message: '截图保存失败：${result.errorMessage}');\n      }\n    } catch (e) {\n      KazumiDialog.showToast(message: '截图失败：$e');\n    }\n  }\n\n  // 启用超分辨率（质量档）时弹出提示\n  Future<void> handleSuperResolutionChange(int shaderIndex) async {\n    if (!mounted) return;\n\n    // mediacodec_embed 不支持超分辨率\n    if (Platform.isAndroid && shaderIndex != 1) {\n      final String androidVideoRenderer =\n          setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto');\n\n      if (androidVideoRenderer == 'mediacodec_embed') {\n        await KazumiDialog.show(builder: (context) {\n          return AlertDialog(\n            title: const Text('兼容性提示'),\n            content: const Text('MediaCodec 渲染器不支持超分辨率功能。\\n\\n'\n                '如需使用超分辨率，请在播放设置中将视频渲染器切换为 gpu 或 gpu-next。'),\n            actions: [\n              TextButton(\n                onPressed: () {\n                  KazumiDialog.dismiss();\n                },\n                child: const Text('确定'),\n              ),\n            ],\n          );\n        });\n        return;\n      }\n    }\n\n    final bool isHighMode = shaderIndex == 3;\n    final bool alreadyShown =\n        setting.get(SettingBoxKey.superResolutionWarn, defaultValue: false);\n\n    if (isHighMode && !alreadyShown) {\n      bool confirmed = false;\n\n      await KazumiDialog.show(builder: (context) {\n        bool dontAskAgain = false;\n\n        return StatefulBuilder(builder: (context, setState) {\n          return AlertDialog(\n            title: const Text('性能提示'),\n            content: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                const Text('启用超分辨率（质量档）可能会造成设备卡顿，是否继续？'),\n                const SizedBox(height: 12),\n                Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Checkbox(\n                      value: dontAskAgain,\n                      onChanged: (value) =>\n                          setState(() => dontAskAgain = value ?? false),\n                    ),\n                    const Text('下次不再询问'),\n                  ],\n                ),\n              ],\n            ),\n            actions: [\n              TextButton(\n                onPressed: () async {\n                  if (dontAskAgain) {\n                    await setting.put(SettingBoxKey.superResolutionWarn, true);\n                  }\n                  KazumiDialog.dismiss();\n                },\n                child: const Text('取消'),\n              ),\n              TextButton(\n                onPressed: () async {\n                  confirmed = true;\n                  if (dontAskAgain) {\n                    await setting.put(SettingBoxKey.superResolutionWarn, true);\n                  }\n                  KazumiDialog.dismiss();\n                },\n                child: const Text('确认'),\n              ),\n            ],\n          );\n        });\n      });\n\n      if (confirmed) {\n        playerController.setShader(shaderIndex);\n      }\n    } else {\n      playerController.setShader(shaderIndex);\n    }\n  }\n\n  void handleFullscreen() {\n    _handleFullscreenChange(context);\n    if (videoPageController.isFullscreen) {\n      Utils.exitFullScreen();\n      if (!Utils.isDesktop()) {\n        widget.locateEpisode();\n        videoPageController.showTabBody = true;\n      }\n    } else {\n      Utils.enterFullScreen();\n      videoPageController.showTabBody = false;\n    }\n    videoPageController.isFullscreen = !videoPageController.isFullscreen;\n  }\n\n  void displayVideoController() {\n    animationController?.forward();\n    hideTimer?.cancel();\n    startHideTimer();\n    playerController.showVideoController = true;\n  }\n\n  void hideVideoController() {\n    animationController?.reverse();\n    hideTimer?.cancel();\n    playerController.showVideoController = false;\n  }\n\n  Future<void> setPlaybackSpeed(double speed) async {\n    await playerController.setPlaybackSpeed(speed);\n  }\n\n  Future<void> handleSpeedChange(String type) async {\n    try {\n      final currentSpeed = playerController.playerSpeed;\n      int index = defaultPlaySpeedList.indexOf(currentSpeed);\n      if (type == \"up\") {\n        if (index < defaultPlaySpeedList.length - 1) {\n          index++;\n          setPlaybackSpeed(defaultPlaySpeedList[index]);\n        } else {\n          KazumiDialog.showToast(message: '已达倍速上限');\n        }\n      } else if (type == \"down\") {\n        if (index > 0) {\n          index--;\n          setPlaybackSpeed(defaultPlaySpeedList[index]);\n        } else {\n          KazumiDialog.showToast(message: '已达倍速下限');\n        }\n      }\n    } catch (e) {\n      KazumiLogger().e('PlayerController: speed change failed', error: e);\n    }\n  }\n\n  Future<void> handleShortcutVolumeChange(String type) async {\n    try {\n      switch (type) {\n        case 'up':\n          await playerController.setVolume(playerController.volume + 10);\n          break;\n        case 'down':\n          await playerController.setVolume(playerController.volume - 10);\n          break;\n        case 'mute':\n          if (playerController.volume > 0) {\n            lastVolume = playerController.volume;\n            await playerController.setVolume(0);\n          } else {\n            await playerController.setVolume(lastVolume);\n          }\n          break;\n        default:\n          return;\n      }\n      playerController.showVolume = true;\n      hideVolumeUITimer?.cancel();\n      hideVolumeUITimer = Timer(const Duration(seconds: 2), () {\n        if (mounted) {\n          playerController.showVolume = false;\n        }\n        hideVolumeUITimer = null;\n      });\n    } catch (e) {\n      KazumiLogger().e('PlayerController: volume change failed', error: e);\n    }\n  }\n\n  Future<void> setBrightness(double value) async {\n    try {\n      await ScreenBrightnessPlatform.instance\n          .setApplicationScreenBrightness(value);\n    } catch (_) {}\n  }\n\n  void startHideTimer() {\n    hideTimer = Timer(const Duration(seconds: 4), () {\n      if (mounted && playerController.canHidePlayerPanel) {\n        playerController.showVideoController = false;\n        animationController?.reverse();\n      }\n      hideTimer = null;\n    });\n  }\n\n  // Used to pass hideTimer operation to panel layer\n  void cancelHideTimer() {\n    hideTimer?.cancel();\n  }\n\n  Timer getPlayerTimer() {\n    return Timer.periodic(const Duration(seconds: 1), (timer) {\n      playerController.playing = playerController.playerPlaying;\n      playerController.isBuffering = playerController.playerBuffering;\n      playerController.currentPosition = playerController.playerPosition;\n      playerController.buffer = playerController.playerBuffer;\n      playerController.duration = playerController.playerDuration;\n      playerController.completed = playerController.playerCompleted;\n      // 弹幕相关\n      if (playerController.currentPosition.inMicroseconds != 0 &&\n          playerController.playerPlaying == true &&\n          playerController.danmakuOn == true) {\n        playerController.danDanmakus[playerController.currentPosition.inSeconds]\n            ?.asMap()\n            .forEach((idx, danmaku) async {\n          if (!_danmakuColor) {\n            danmaku.color = Colors.white;\n          }\n          if (!_danmakuBiliBiliSource && danmaku.source.contains('BiliBili')) {\n            return;\n          }\n          if (!_danmakuGamerSource && danmaku.source.contains('Gamer')) {\n            return;\n          }\n          if (!_danmakuDanDanSource &&\n              !(danmaku.source.contains('BiliBili') ||\n                  danmaku.source.contains('Gamer'))) {\n            return;\n          }\n          await Future.delayed(\n              Duration(\n                  milliseconds: idx *\n                      1000 ~/\n                      playerController\n                          .danDanmakus[\n                              playerController.currentPosition.inSeconds]!\n                          .length),\n              () => mounted &&\n                      playerController.playerPlaying &&\n                      !playerController.playerBuffering &&\n                      playerController.danmakuOn &&\n                      !myController.isDanmakuBlocked(danmaku.message)\n                  ? playerController.danmakuController.addDanmaku(\n                      DanmakuContentItem(danmaku.message,\n                          color: danmaku.color,\n                          type: danmaku.type == 4\n                              ? DanmakuItemType.bottom\n                              : (danmaku.type == 5\n                                  ? DanmakuItemType.top\n                                  : DanmakuItemType.scroll)))\n                  : null);\n        });\n      }\n      // 音量相关\n      if (!playerController.volumeSeeking) {\n        if (Utils.isDesktop()) {\n          playerController.volume = playerController.playerVolume;\n        } else {\n          FlutterVolumeController.getVolume().then((value) {\n            final volume = value ?? 0.0;\n            playerController.volume = volume * 100;\n          });\n        }\n      }\n      // 亮度相关\n      if (!Platform.isWindows &&\n          !Platform.isMacOS &&\n          !Platform.isLinux &&\n          !playerController.brightnessSeeking) {\n        ScreenBrightnessPlatform.instance.application.then((value) {\n          playerController.brightness = value;\n        });\n      }\n      // 历史记录相关\n      if (playerController.playerPlaying && !videoPageController.loading && !videoPageController.isOfflineMode) {\n        if (!WebDav().isHistorySyncing) {\n          final pluginName = videoPageController.isOfflineMode\n              ? videoPageController.offlinePluginName\n              : videoPageController.currentPlugin.name;\n          historyController.updateHistory(\n              videoPageController.actualEpisodeNumber,\n              videoPageController.currentRoad,\n              pluginName,\n              videoPageController.bangumiItem,\n              playerController.playerPosition,\n              videoPageController.src,\n              videoPageController.roadList[videoPageController.currentRoad]\n                  .identifier[videoPageController.currentEpisode - 1]);\n        }\n      }\n      // 自动播放下一集\n      if (playerController.completed &&\n          videoPageController.currentEpisode <\n              videoPageController\n                  .roadList[videoPageController.currentRoad].data.length &&\n          !videoPageController.loading &&\n          autoPlayNext) {\n        KazumiDialog.showToast(\n            message:\n                '正在加载${videoPageController.roadList[videoPageController.currentRoad].identifier[videoPageController.currentEpisode]}');\n        try {\n          playerTimer!.cancel();\n        } catch (_) {}\n        widget.changeEpisode(videoPageController.currentEpisode + 1,\n            currentRoad: videoPageController.currentRoad);\n      }\n      // 一起去看相关\n      playerController.setSyncPlayCurrentPosition();\n    });\n  }\n\n  void showDanmakuSearchDialog(String keyword) async {\n    KazumiDialog.dismiss();\n    KazumiDialog.showLoading(msg: '弹幕检索中');\n    DanmakuSearchResponse danmakuSearchResponse;\n    DanmakuEpisodeResponse danmakuEpisodeResponse;\n    try {\n      danmakuSearchResponse =\n          await DanmakuRequest.getDanmakuSearchResponse(keyword);\n    } catch (e) {\n      KazumiDialog.dismiss();\n      KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}');\n      return;\n    }\n    KazumiDialog.dismiss();\n    if (danmakuSearchResponse.animes.isEmpty) {\n      KazumiDialog.showToast(message: '未找到匹配结果');\n      return;\n    }\n    await KazumiDialog.show(builder: (context) {\n      return Dialog(\n        child: ConstrainedBox(\n          constraints: const BoxConstraints(maxWidth: 560),\n          child: ListView(\n            shrinkWrap: true,\n            children: danmakuSearchResponse.animes.map((danmakuInfo) {\n              return ListTile(\n                title: Text(danmakuInfo.animeTitle),\n                onTap: () async {\n                  KazumiDialog.dismiss();\n                  KazumiDialog.showLoading(msg: '弹幕检索中');\n                  try {\n                    danmakuEpisodeResponse =\n                        await DanmakuRequest.getDanDanEpisodesByDanDanBangumiID(\n                            danmakuInfo.animeId);\n                  } catch (e) {\n                    KazumiDialog.dismiss();\n                    KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}');\n                    return;\n                  }\n                  KazumiDialog.dismiss();\n                  if (danmakuEpisodeResponse.episodes.isEmpty) {\n                    KazumiDialog.showToast(message: '未找到匹配结果');\n                    return;\n                  }\n                  KazumiDialog.show(builder: (context) {\n                    return Dialog(\n                      child: ConstrainedBox(\n                        constraints: const BoxConstraints(maxWidth: 560),\n                        child: ListView(\n                          shrinkWrap: true,\n                          children:\n                              danmakuEpisodeResponse.episodes.map((episode) {\n                            return ListTile(\n                              title: Text(episode.episodeTitle),\n                              onTap: () async {\n                                KazumiDialog.dismiss();\n                                try {\n                                  await playerController\n                                      .getDanDanmakuByEpisodeID(\n                                          episode.episodeId);\n                                  KazumiDialog.showToast(message: '弹幕切换成功');\n                                } catch (e) {\n                                  KazumiDialog.showToast(message: '弹幕切换失败');\n                                }\n                              },\n                            );\n                          }).toList(),\n                        ),\n                      ),\n                    );\n                  });\n                },\n              );\n            }).toList(),\n          ),\n        ),\n      );\n    });\n  }\n\n  // 弹幕查询\n  void showDanmakuSwitch() {\n    KazumiDialog.show(\n      builder: (context) {\n        final TextEditingController searchTextController =\n            TextEditingController();\n        searchTextController.text = videoPageController.title;\n        return AlertDialog(\n          title: const Text('弹幕检索'),\n          content: TextField(\n            controller: searchTextController,\n            decoration: const InputDecoration(\n              hintText: '番剧名',\n            ),\n            onSubmitted: (keyword) {\n              showDanmakuSearchDialog(keyword);\n            },\n          ),\n          actions: [\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n                widget.keyboardFocus.requestFocus();\n              },\n              child: Text(\n                '取消',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n              onPressed: () {\n                showDanmakuSearchDialog(searchTextController.text);\n              },\n              child: const Text(\n                '提交',\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget get videoInfoBody {\n    return Observer(builder: (context) {\n      return ListView(\n        children: [\n          ListTile(\n            title: const Text(\"Source\"),\n            subtitle: Text(playerController.videoUrl),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(text: playerController.videoUrl),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"Resolution\"),\n            subtitle: Text(\n                '${playerController.playerWidth}x${playerController.playerHeight}'),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"Resolution\\n${playerController.playerWidth}x${playerController.playerHeight}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"VideoParams\"),\n            subtitle: Text(playerController.playerVideoParams.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"VideoParams\\n${playerController.playerVideoParams.toString()}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"AudioParams\"),\n            subtitle: Text(playerController.playerAudioParams.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"AudioParams\\n${playerController.playerAudioParams.toString()}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"Media\"),\n            subtitle: Text(playerController.playerPlaylist.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text: \"Media\\n${playerController.playerPlaylist.toString()}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"AudioTrack\"),\n            subtitle: Text(playerController.playerAudioTracks.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"AudioTrack\\n${playerController.playerAudioTracks.toString()}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"VideoTrack\"),\n            subtitle: Text(playerController.playerVideoTracks.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"VideoTrack\\n${playerController.playerVideoTracks.toString()}\",\n                ),\n              );\n            },\n          ),\n          ListTile(\n            title: const Text(\"AudioBitrate\"),\n            subtitle: Text(playerController.playerAudioBitrate.toString()),\n            onTap: () {\n              KazumiDialog.showToast(message: '已复制到剪贴板');\n              Clipboard.setData(\n                ClipboardData(\n                  text:\n                      \"AudioBitrate\\n${playerController.playerAudioBitrate.toString()}\",\n                ),\n              );\n            },\n          ),\n        ],\n      );\n    });\n  }\n\n  Widget get videoDebugLogBody {\n    return Scaffold(\n      body: Padding(\n        padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 0),\n        child: Observer(builder: (context) {\n          return ListView.builder(\n            itemCount: playerController.playerLog.length,\n            itemBuilder: (context, index) {\n              return Text(playerController.playerLog[index]);\n            },\n          );\n        }),\n      ),\n      floatingActionButton: FloatingActionButton(\n          child: const Icon(Icons.copy),\n          onPressed: () {\n            Clipboard.setData(\n              ClipboardData(text: playerController.playerLog.join('\\n')),\n            );\n          }),\n    );\n  }\n\n  void showVideoInfo() async {\n    showModalBottomSheet(\n        isScrollControlled: true,\n        constraints: BoxConstraints(\n            maxHeight: MediaQuery.of(context).size.height * 3 / 4,\n            maxWidth: (Utils.isDesktop() || Utils.isTablet())\n                ? MediaQuery.of(context).size.width * 9 / 16\n                : MediaQuery.of(context).size.width),\n        clipBehavior: Clip.antiAlias,\n        context: context,\n        builder: (context) {\n          return DefaultTabController(\n            length: 2,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  const PreferredSize(\n                    preferredSize: Size.fromHeight(kToolbarHeight),\n                    child: Material(\n                      child: TabBar(\n                        tabs: [\n                          Tab(text: '状态'),\n                          Tab(text: '日志'),\n                        ],\n                      ),\n                    ),\n                  ),\n                  Expanded(\n                    child: TabBarView(\n                      children: [\n                        videoInfoBody,\n                        videoDebugLogBody,\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n        });\n  }\n\n  void showSyncPlayEndPointSwitchDialog() {\n    if (playerController.syncplayController != null) {\n      KazumiDialog.showToast(message: 'SyncPlay: 请先退出当前房间再切换服务器');\n      return;\n    }\n\n    final String defaultCustomSyncPlayEndPoint = '自定义服务器';\n    String customSyncPlayEndPoint = defaultCustomSyncPlayEndPoint;\n    String selectedSyncPlayEndPoint = setting.get(\n        SettingBoxKey.syncPlayEndPoint,\n        defaultValue: defaultSyncPlayEndPoint);\n\n    KazumiDialog.show(\n      builder: (context) {\n        return StatefulBuilder(builder: (context, setDialogState) {\n          List<String> syncPlayEndPoints = [];\n          syncPlayEndPoints.addAll(defaultSyncPlayEndPoints);\n          syncPlayEndPoints.add(customSyncPlayEndPoint);\n          if (!syncPlayEndPoints.contains(selectedSyncPlayEndPoint)) {\n            syncPlayEndPoints.add(selectedSyncPlayEndPoint);\n          }\n          return AlertDialog(\n            title: const Text('选择服务器'),\n            content: SingleChildScrollView(\n              child: ListBody(\n                children: <Widget>[\n                  DropdownButtonFormField<String>(\n                    decoration: InputDecoration(\n                      border: OutlineInputBorder(),\n                    ),\n                    isExpanded: true,\n                    value: selectedSyncPlayEndPoint,\n                    items: syncPlayEndPoints.map((String value) {\n                      return DropdownMenuItem<String>(\n                        value: value,\n                        child: Text(\n                          value,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      );\n                    }).toList(),\n                    selectedItemBuilder: (context) {\n                      return syncPlayEndPoints.map((String value) {\n                        return Text(\n                          value,\n                          overflow: TextOverflow.ellipsis,\n                        );\n                      }).toList();\n                    },\n                    onChanged: (String? newValue) {\n                      if (newValue != null) {\n                        if (newValue == defaultCustomSyncPlayEndPoint) {\n                          final serverTextController = TextEditingController();\n                          KazumiDialog.show(\n                            builder: (context) {\n                              return AlertDialog(\n                                title: const Text('自定义服务器'),\n                                content: TextField(\n                                  controller: serverTextController,\n                                  decoration: const InputDecoration(\n                                    hintText: '请输入服务器地址',\n                                  ),\n                                ),\n                                actions: <Widget>[\n                                  TextButton(\n                                    child: const Text('取消'),\n                                    onPressed: () {\n                                      KazumiDialog.dismiss();\n                                    },\n                                  ),\n                                  TextButton(\n                                    child: const Text('确认'),\n                                    onPressed: () {\n                                      if (serverTextController\n                                              .text.isNotEmpty &&\n                                          !syncPlayEndPoints.contains(\n                                              serverTextController.text)) {\n                                        KazumiDialog.dismiss();\n                                        setDialogState(() {\n                                          customSyncPlayEndPoint =\n                                              serverTextController.text;\n                                          selectedSyncPlayEndPoint =\n                                              serverTextController.text;\n                                        });\n                                      } else {\n                                        KazumiDialog.showToast(\n                                            message: '服务器地址不能重复或为空');\n                                      }\n                                    },\n                                  ),\n                                ],\n                              );\n                            },\n                          );\n                        } else {\n                          setDialogState(() {\n                            selectedSyncPlayEndPoint = newValue;\n                          });\n                        }\n                      }\n                    },\n                  ),\n                ],\n              ),\n            ),\n            actions: <Widget>[\n              TextButton(\n                child: const Text('取消'),\n                onPressed: () {\n                  KazumiDialog.dismiss();\n                },\n              ),\n              TextButton(\n                child: const Text('确认'),\n                onPressed: () {\n                  setting.put(\n                    SettingBoxKey.syncPlayEndPoint,\n                    selectedSyncPlayEndPoint,\n                  );\n                  KazumiDialog.dismiss();\n                },\n              ),\n            ],\n          );\n        });\n      },\n    );\n  }\n\n  void showSyncPlayRoomCreateDialog() {\n    final formKey = GlobalKey<FormState>();\n    final TextEditingController roomController = TextEditingController();\n    final TextEditingController usernameController = TextEditingController();\n    KazumiDialog.show(builder: (BuildContext context) {\n      return AlertDialog(\n        title: const Text('加入房间'),\n        content: Form(\n          key: formKey,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              TextFormField(\n                controller: roomController,\n                keyboardType: TextInputType.number,\n                decoration: const InputDecoration(\n                  labelText: '房间号',\n                ),\n                validator: (value) {\n                  if (value == null || value.isEmpty) {\n                    return '请输入房间号';\n                  }\n                  final regex = RegExp(r'^[0-9]{6,10}$');\n                  if (!regex.hasMatch(value)) {\n                    return '房间号需要6到10位数字';\n                  }\n                  return null;\n                },\n              ),\n              const SizedBox(height: 16),\n              TextFormField(\n                controller: usernameController,\n                decoration: const InputDecoration(\n                  labelText: '用户名',\n                ),\n                validator: (value) {\n                  if (value == null || value.isEmpty) {\n                    return '请输入用户名';\n                  }\n                  final regex = RegExp(r'^[a-zA-Z]{4,12}$');\n                  if (!regex.hasMatch(value)) {\n                    return '用户名必须为4到12位英文字符';\n                  }\n                  return null;\n                },\n              ),\n            ],\n          ),\n        ),\n        actions: [\n          TextButton(\n            onPressed: () {\n              KazumiDialog.dismiss();\n            },\n            child: const Text('取消'),\n          ),\n          TextButton(\n            onPressed: () {\n              if (formKey.currentState!.validate()) {\n                KazumiDialog.dismiss();\n                playerController.createSyncPlayRoom(roomController.text,\n                    usernameController.text, widget.changeEpisode);\n              }\n            },\n            child: const Text('确定'),\n          ),\n        ],\n      );\n    });\n  }\n\n  /// Used to decide which panel is used.\n  /// It's too complicated to write these in conditional sentence.\n  /// * true: use [PlayerItemPanel]\n  /// * false: use [SmallestPlayerItemPanel]\n  bool needFullPanel(BuildContext context) {\n    // windows too small, workaround for ohos floating window\n    if (MediaQuery.sizeOf(context).width < LayoutBreakpoint.compact['width']!) {\n      return false;\n    }\n    // in desktop pip mode\n    if (videoPageController.isPip) {\n      return false;\n    }\n    // does not meet Google's phone landscape height and tablet landscape width requirements.\n    if (!Utils.isDesktop() &&\n        (MediaQuery.sizeOf(context).height >\n                LayoutBreakpoint.compact['height']! &&\n            MediaQuery.sizeOf(context).width <\n                LayoutBreakpoint.medium['width']!)) {\n      return false;\n    }\n    if (Utils.isDesktop() &&\n        (MediaQuery.sizeOf(context).height >\n                LayoutBreakpoint.compact['height']! &&\n            MediaQuery.sizeOf(context).width <\n                LayoutBreakpoint.compact['width']!)) {\n      return false;\n    }\n    return true;\n  }\n\n  @override\n  void onWindowRestore() {\n    playerController.danmakuController.clear();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _loadShortcuts();\n    _initKeyboardActions();\n    _initPlayerMenu();\n    _fullscreenListener = mobx.reaction<bool>(\n      (_) => videoPageController.isFullscreen,\n      (_) {\n        _handleFullscreenChange(context);\n      },\n    );\n    // workaround for #214\n    if (Platform.isIOS) {\n      FlutterVolumeController.setIOSAudioSessionCategory(\n          category: AudioSessionCategory.playback);\n    }\n    WidgetsBinding.instance.addObserver(this);\n    animationController ??= AnimationController(\n      duration: const Duration(milliseconds: 300),\n      vsync: this,\n    );\n    webDavEnable = setting.get(SettingBoxKey.webDavEnable, defaultValue: false);\n    webDavEnableHistory =\n        setting.get(SettingBoxKey.webDavEnableHistory, defaultValue: false);\n    playerController.danmakuOn =\n        setting.get(SettingBoxKey.danmakuEnabledByDefault, defaultValue: false);\n    _border = setting.get(SettingBoxKey.danmakuBorder, defaultValue: true);\n    _opacity = setting.get(SettingBoxKey.danmakuOpacity, defaultValue: 1.0);\n    _fontSize = setting.get(SettingBoxKey.danmakuFontSize,\n        defaultValue: (Utils.isCompact()) ? 16.0 : 25.0);\n    _danmakuArea = setting.get(SettingBoxKey.danmakuArea, defaultValue: 1.0);\n    _hideTop = !setting.get(SettingBoxKey.danmakuTop, defaultValue: true);\n    _hideBottom =\n        !setting.get(SettingBoxKey.danmakuBottom, defaultValue: false);\n    _hideScroll = !setting.get(SettingBoxKey.danmakuScroll, defaultValue: true);\n    _massiveMode =\n        setting.get(SettingBoxKey.danmakuMassive, defaultValue: false);\n    _danmakuColor = setting.get(SettingBoxKey.danmakuColor, defaultValue: true);\n    _danmakuDuration =\n        setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0);\n    _danmakuLineHeight =\n        setting.get(SettingBoxKey.danmakuLineHeight, defaultValue: 1.6);\n    _danmakuBiliBiliSource =\n        setting.get(SettingBoxKey.danmakuBiliBiliSource, defaultValue: true);\n    _danmakuGamerSource =\n        setting.get(SettingBoxKey.danmakuGamerSource, defaultValue: true);\n    _danmakuDanDanSource =\n        setting.get(SettingBoxKey.danmakuDanDanSource, defaultValue: true);\n    _danmakuFontWeight =\n        setting.get(SettingBoxKey.danmakuFontWeight, defaultValue: 4);\n    _danmakuUseSystemFont =\n        setting.get(SettingBoxKey.useSystemFont, defaultValue: false);\n    _danmakuBorderSize = \n        setting.get(SettingBoxKey.danmakuBorderSize, defaultValue: 1.5);\n    haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true);\n    autoPlayNext = setting.get(SettingBoxKey.autoPlayNext, defaultValue: true);\n    playerTimer = getPlayerTimer();\n    windowManager.addListener(this);\n    displayVideoController();\n  }\n\n  @override\n  void dispose() {\n    // Don't dispose player here\n    // We need to reuse the player after episode is changed and player item is disposed\n    // We dispose player after video page disposed\n    _fullscreenListener();\n    WidgetsBinding.instance.removeObserver(this);\n    windowManager.removeListener(this);\n    playerTimer?.cancel();\n    hideTimer?.cancel();\n    mouseScrollerTimer?.cancel();\n    hideVolumeUITimer?.cancel();\n    animationController?.dispose();\n    animationController = null;\n    _disposePlayerMenu();\n    // Reset player panel state\n    playerController.lockPanel = false;\n    playerController.showVideoController = true;\n    playerController.showSeekTime = false;\n    playerController.showBrightness = false;\n    playerController.showVolume = false;\n    playerController.showPlaySpeed = false;\n    playerController.brightnessSeeking = false;\n    playerController.volumeSeeking = false;\n    playerController.canHidePlayerPanel = true;\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    collectType =\n        collectController.getCollectType(videoPageController.bangumiItem);\n    return Observer(\n      builder: (context) {\n        return ClipRect(\n          child: Container(\n            color: Colors.black,\n            child: MouseRegion(\n              cursor: (videoPageController.isFullscreen &&\n                      !playerController.showVideoController)\n                  ? SystemMouseCursors.none\n                  : SystemMouseCursors.basic,\n              onHover: (PointerEvent pointerEvent) {\n                // workaround for android.\n                // I don't know why, but android tap event will trigger onHover event.\n                if (Utils.isDesktop()) {\n                  if (pointerEvent.position.dy > 50 &&\n                      pointerEvent.position.dy <\n                          MediaQuery.of(context).size.height - 70) {\n                    _handleHove();\n                  } else {\n                    if (!playerController.showVideoController) {\n                      animationController?.forward();\n                      playerController.showVideoController = true;\n                    }\n                  }\n                }\n              },\n              child: Listener(\n                onPointerSignal: (pointerSignal) {\n                  if (pointerSignal is PointerScrollEvent) {\n                    _handleMouseScroller();\n                    final scrollDelta = pointerSignal.scrollDelta;\n                    final double volume =\n                        playerController.volume - scrollDelta.dy / 60;\n                    playerController.setVolume(volume);\n                  }\n                },\n                child: SizedBox(\n                  height: videoPageController.isFullscreen\n                      ? (MediaQuery.of(context).size.height)\n                      : (MediaQuery.of(context).size.width * 9.0 / (16.0)),\n                  width: MediaQuery.of(context).size.width,\n                  child: Stack(alignment: Alignment.center, children: [\n                    Center(\n                        child: Focus(\n                            // workaround for #461\n                            // I don't know why, but the focus node will break popscope.\n                            focusNode: widget.keyboardFocus,\n                            autofocus: true,\n                            onKeyEvent: (focusNode, KeyEvent event) {\n                              bool handled = false;\n                              final keyLabel =\n                                  event.logicalKey.keyLabel.isNotEmpty\n                                      ? event.logicalKey.keyLabel\n                                      : event.logicalKey.debugName ?? '';\n                              if (event is KeyDownEvent) {\n                                handled = handleShortcutDown(keyLabel);\n                              } else if (event is KeyRepeatEvent) {\n                                handled =\n                                    handleShortcutLongPress(keyLabel, \"Repeat\");\n                              } else if (event is KeyUpEvent) {\n                                handled =\n                                    handleShortcutLongPress(keyLabel, \"Up\");\n                              }\n                              return handled\n                                  ? KeyEventResult.handled\n                                  : KeyEventResult.ignored;\n                            },\n                            child: const PlayerItemSurface())),\n                    (playerController.isBuffering ||\n                            videoPageController.loading)\n                        ? const Positioned.fill(\n                            child: Center(\n                              child: CircularProgressIndicator(),\n                            ),\n                          )\n                        : Container(),\n                    GestureDetector(\n                      onTap: () {\n                        _handleTap();\n                      },\n                      onDoubleTap: (playerController.lockPanel)\n                          ? null\n                          : () {\n                              _handleDoubleTap();\n                            },\n                      onLongPressStart: (_) {\n                        if (playerController.lockPanel) {\n                          return;\n                        }\n                        setState(() {\n                          playerController.showPlaySpeed = true;\n                        });\n                        lastPlayerSpeed = playerController.playerSpeed;\n                        setPlaybackSpeed(2.0);\n                      },\n                      onLongPressEnd: (_) {\n                        if (playerController.lockPanel) {\n                          return;\n                        }\n                        setState(() {\n                          playerController.showPlaySpeed = false;\n                        });\n                        setPlaybackSpeed(lastPlayerSpeed);\n                      },\n                      child: Container(\n                        color: Colors.transparent,\n                        width: double.infinity,\n                        height: double.infinity,\n                      ),\n                    ),\n                    // 弹幕面板\n                    Positioned(\n                      top: 0,\n                      left: 0,\n                      right: 0,\n                      height: videoPageController.isFullscreen\n                          ? MediaQuery.sizeOf(context).height\n                          : (MediaQuery.sizeOf(context).width * 9 / 16),\n                      child: DanmakuScreen(\n                        key: _danmuKey,\n                        createdController: (DanmakuController e) {\n                          playerController.danmakuController = e;\n                          WidgetsBinding.instance.addPostFrameCallback((_) {\n                            playerController.updateDanmakuSpeed();\n                          });\n                        },\n                        option: DanmakuOption(\n                          hideTop: _hideTop,\n                          hideScroll: _hideScroll,\n                          hideBottom: _hideBottom,\n                          area: _danmakuArea,\n                          opacity: _opacity,\n                          fontSize: _fontSize,\n                          duration:\n                              _danmakuDuration / playerController.playerSpeed,\n                          lineHeight: _danmakuLineHeight,\n                          strokeWidth: _border ? _danmakuBorderSize : 0.0,\n                          fontWeight: _danmakuFontWeight,\n                          massiveMode: _massiveMode,\n                          fontFamily: _danmakuUseSystemFont\n                              ? null\n                              : customAppFontFamily,\n                        ),\n                      ),\n                    ),\n                    // 播放器控制面板\n                    (needFullPanel(context))\n                        ? PlayerItemPanel(\n                            onBackPressed: widget.onBackPressed,\n                            setPlaybackSpeed: setPlaybackSpeed,\n                            showDanmakuSwitch: showDanmakuSwitch,\n                            changeEpisode: widget.changeEpisode,\n                            openMenu: widget.openMenu,\n                            handleFullscreen: handleFullscreen,\n                            handleProgressBarDragStart:\n                                handleProgressBarDragStart,\n                            handleProgressBarDragEnd: handleProgressBarDragEnd,\n                            handleSuperResolutionChange:\n                                handleSuperResolutionChange,\n                            handlePreNextEpisode: handlePreNextEpisode,\n                            animationController: animationController!,\n                            keyboardFocus: widget.keyboardFocus,\n                            sendDanmaku: widget.sendDanmaku,\n                            startHideTimer: startHideTimer,\n                            cancelHideTimer: cancelHideTimer,\n                            handleDanmaku: handleDanmaku,\n                            showVideoInfo: showVideoInfo,\n                            showSyncPlayRoomCreateDialog:\n                                showSyncPlayRoomCreateDialog,\n                            showSyncPlayEndPointSwitchDialog:\n                                showSyncPlayEndPointSwitchDialog,\n                            showDanmakuDestinationPickerAndSend:\n                                widget.showDanmakuDestinationPickerAndSend,\n                            pauseForTimedShutdown: widget.pauseForTimedShutdown,\n                            disableAnimations: widget.disableAnimations,\n                            handleScreenShot: handleScreenshot,\n                            skipOP: skipOP,\n                          )\n                        : SmallestPlayerItemPanel(\n                            onBackPressed: widget.onBackPressed,\n                            setPlaybackSpeed: setPlaybackSpeed,\n                            showDanmakuSwitch: showDanmakuSwitch,\n                            handleFullscreen: handleFullscreen,\n                            handleProgressBarDragStart:\n                                handleProgressBarDragStart,\n                            handleProgressBarDragEnd: handleProgressBarDragEnd,\n                            handleSuperResolutionChange:\n                                handleSuperResolutionChange,\n                            animationController: animationController!,\n                            keyboardFocus: widget.keyboardFocus,\n                            handleHove: _handleHove,\n                            startHideTimer: startHideTimer,\n                            cancelHideTimer: cancelHideTimer,\n                            handleDanmaku: handleDanmaku,\n                            showVideoInfo: showVideoInfo,\n                            showSyncPlayRoomCreateDialog:\n                                showSyncPlayRoomCreateDialog,\n                            showSyncPlayEndPointSwitchDialog:\n                                showSyncPlayEndPointSwitchDialog,\n                            pauseForTimedShutdown: widget.pauseForTimedShutdown,\n                            disableAnimations: widget.disableAnimations,\n                            skipOP: skipOP,\n                          ),\n                    // 播放器手势控制\n                    Positioned.fill(\n                      left: 16,\n                      top: 25,\n                      right: 15,\n                      bottom: 15,\n                      child: (Utils.isDesktop() || playerController.lockPanel)\n                          ? Container()\n                          : GestureDetector(\n                              onHorizontalDragStart: (_) {\n                                if (!playerController.showVideoController) {\n                                  animationController?.forward();\n                                }\n                                playerController.canHidePlayerPanel = false;\n                              },\n                              onHorizontalDragUpdate:\n                                  (DragUpdateDetails details) {\n                                playerController.showSeekTime = true;\n                                playerTimer?.cancel();\n                                playerController.pause(enableSync: false);\n                                final double scale =\n                                    180000 / MediaQuery.sizeOf(context).width;\n                                int ms = (playerController\n                                            .currentPosition.inMilliseconds +\n                                        (details.delta.dx * scale).round())\n                                    .clamp(\n                                        0,\n                                        playerController\n                                            .duration.inMilliseconds);\n                                playerController.currentPosition =\n                                    Duration(milliseconds: ms);\n                              },\n                              onHorizontalDragEnd: (_) {\n                                playerController.play(enableSync: false);\n                                playerController\n                                    .seek(playerController.currentPosition);\n                                playerController.canHidePlayerPanel = true;\n                                if (!playerController.showVideoController) {\n                                  animationController?.reverse();\n                                } else {\n                                  hideTimer?.cancel();\n                                  startHideTimer();\n                                }\n                                playerTimer?.cancel();\n                                playerTimer = getPlayerTimer();\n                                playerController.showSeekTime = false;\n                              },\n                              onVerticalDragUpdate:\n                                  (DragUpdateDetails details) async {\n                                final double totalWidth =\n                                    MediaQuery.sizeOf(context).width;\n                                final double totalHeight =\n                                    MediaQuery.sizeOf(context).height;\n                                final double tapPosition =\n                                    details.localPosition.dx;\n                                final double sectionWidth = totalWidth / 2;\n                                final double delta = details.delta.dy;\n\n                                if (tapPosition < sectionWidth) {\n                                  // 左边区域\n                                  playerController.brightnessSeeking = true;\n                                  playerController.showBrightness = true;\n                                  final double level = (totalHeight) * 2;\n                                  final double brightness =\n                                      playerController.brightness -\n                                          delta / level;\n                                  final double result =\n                                      brightness.clamp(0.0, 1.0);\n                                  setBrightness(result);\n                                  playerController.brightness = result;\n                                } else {\n                                  // 右边区域\n                                  playerController.volumeSeeking = true;\n                                  playerController.showVolume = true;\n                                  final double level = (totalHeight) * 0.03;\n                                  final double volume =\n                                      playerController.volume - delta / level;\n                                  playerController.setVolume(volume);\n                                }\n                              },\n                              onVerticalDragEnd: (_) {\n                                if (playerController.volumeSeeking) {\n                                  playerController.volumeSeeking = false;\n                                  Future.delayed(const Duration(seconds: 1),\n                                      () {\n                                    FlutterVolumeController.updateShowSystemUI(\n                                        true);\n                                  });\n                                }\n                                if (playerController.brightnessSeeking) {\n                                  playerController.brightnessSeeking = false;\n                                }\n                                playerController.showVolume = false;\n                                playerController.showBrightness = false;\n                              },\n                            ),\n                    ),\n                  ]),\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/player_item_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/utils/remote.dart';\nimport 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb;\nimport 'package:kazumi/pages/settings/danmaku/danmaku_settings_sheet.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:audio_video_progress_bar/audio_video_progress_bar.dart';\nimport 'package:kazumi/utils/timed_shutdown_service.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\n\nclass PlayerItemPanel extends StatefulWidget {\n  const PlayerItemPanel({\n    super.key,\n    required this.onBackPressed,\n    required this.setPlaybackSpeed,\n    required this.showDanmakuSwitch,\n    required this.changeEpisode,\n    required this.handleFullscreen,\n    required this.handleScreenShot,\n    required this.handlePreNextEpisode,\n    required this.handleProgressBarDragStart,\n    required this.handleProgressBarDragEnd,\n    required this.handleSuperResolutionChange,\n    required this.animationController,\n    required this.openMenu,\n    required this.keyboardFocus,\n    required this.sendDanmaku,\n    required this.startHideTimer,\n    required this.cancelHideTimer,\n    required this.handleDanmaku,\n    required this.skipOP,\n    required this.showVideoInfo,\n    required this.showSyncPlayRoomCreateDialog,\n    required this.showSyncPlayEndPointSwitchDialog,\n    required this.showDanmakuDestinationPickerAndSend,\n    required this.pauseForTimedShutdown,\n    this.disableAnimations = false,\n  });\n\n  final void Function(BuildContext) onBackPressed;\n  final Future<void> Function(double) setPlaybackSpeed;\n  final void Function() showDanmakuSwitch;\n  final Future<void> Function(int, {int currentRoad, int offset}) changeEpisode;\n  final void Function() openMenu;\n  final void Function() handleFullscreen;\n  final void Function() handleScreenShot;\n  final void Function(ThumbDragDetails details) handleProgressBarDragStart;\n  final void Function() handleProgressBarDragEnd;\n  final Future<void> Function(int shaderIndex) handleSuperResolutionChange;\n  final AnimationController animationController;\n  final FocusNode keyboardFocus;\n  final void Function() startHideTimer;\n  final void Function() cancelHideTimer;\n  final void Function() handleDanmaku;\n  final void Function(String direction) handlePreNextEpisode;\n  final void Function() skipOP;\n  final void Function(String) sendDanmaku;\n  final void Function() showVideoInfo;\n  final void Function() showSyncPlayRoomCreateDialog;\n  final void Function() showSyncPlayEndPointSwitchDialog;\n  final void Function(String) showDanmakuDestinationPickerAndSend;\n  final VoidCallback pauseForTimedShutdown;\n  final bool disableAnimations;\n\n  @override\n  State<PlayerItemPanel> createState() => _PlayerItemPanelState();\n}\n\nclass _PlayerItemPanelState extends State<PlayerItemPanel> {\n  Box setting = GStorage.setting;\n  late bool haEnable;\n  late Animation<Offset> topOffsetAnimation;\n  late Animation<Offset> bottomOffsetAnimation;\n  late Animation<Offset> leftOffsetAnimation;\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final PlayerController playerController = Modular.get<PlayerController>();\n  final DownloadController downloadController = Modular.get<DownloadController>();\n  final TextEditingController textController = TextEditingController();\n  final FocusNode textFieldFocus = FocusNode();  \n  // SVG Caches\n  String? cachedSvgString;\n  Widget? cachedDanmakuOnIcon;\n  Widget? cachedDanmakuOffIcon;\n  Widget? cachedDanmakuSettingIcon;\n\n  static const double _danmakuIconSize = 24.0;\n  static const double _loadingIndicatorStrokeWidth = 2.0;\n\n  Widget get danmakuTextField {\n    return Container(\n      constraints: Utils.isDesktop()\n          ? const BoxConstraints(maxWidth: 500, maxHeight: 33)\n          : const BoxConstraints(maxHeight: 33),\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      child: TextField(\n        focusNode: textFieldFocus,\n        style: TextStyle(\n            fontSize: Utils.isDesktop() ? 15 : 13, color: Colors.white),\n        controller: textController,\n        textAlignVertical: TextAlignVertical.center,\n        decoration: InputDecoration(\n          enabled: playerController.danmakuOn,\n          filled: true,\n          fillColor: Colors.white38,\n          floatingLabelBehavior: FloatingLabelBehavior.never,\n          hintText: playerController.danmakuOn ? '发个友善的弹幕见证当下' : '已关闭弹幕',\n          hintStyle: TextStyle(\n              fontSize: Utils.isDesktop() ? 15 : 13, color: Colors.white60),\n          alignLabelWithHint: true,\n          contentPadding: EdgeInsets.symmetric(\n              vertical: 8, horizontal: Utils.isDesktop() ? 8 : 12),\n          border: OutlineInputBorder(\n            borderSide: BorderSide.none,\n            borderRadius:\n                BorderRadius.all(Radius.circular(Utils.isDesktop() ? 8 : 20)),\n          ),\n          suffixIconConstraints: const BoxConstraints(minWidth: 0),\n          suffixIcon: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              TextButton(\n                onPressed: () {\n                  textFieldFocus.unfocus();\n                  widget.showDanmakuDestinationPickerAndSend(textController.text);\n                  textController.clear();\n                },\n                style: TextButton.styleFrom(\n                  foregroundColor: playerController.danmakuOn\n                      ? Theme.of(context).colorScheme.onPrimaryContainer\n                      : Colors.white60,\n                  backgroundColor: playerController.danmakuOn\n                      ? Theme.of(context).colorScheme.primaryContainer\n                      : Theme.of(context).disabledColor,\n                  shape: RoundedRectangleBorder(\n                    borderRadius: BorderRadius.circular(Utils.isDesktop() ? 8 : 20),\n                  ),\n                ),\n                child: const Text('发送'),\n              ),\n            ],\n          ),\n        ),\n        onTapAlwaysCalled: true,\n        onTap: () {\n          widget.cancelHideTimer();\n          playerController.canHidePlayerPanel = false;\n        },\n        onSubmitted: (msg) {\n          textFieldFocus.unfocus();\n          widget.showDanmakuDestinationPickerAndSend(msg);\n          widget.cancelHideTimer();\n          widget.startHideTimer();\n          playerController.canHidePlayerPanel = true;\n          textController.clear();\n        },\n        onTapOutside: (_) {\n          widget.cancelHideTimer();\n          widget.startHideTimer();\n          playerController.canHidePlayerPanel = true;\n          textFieldFocus.unfocus();\n          widget.keyboardFocus.requestFocus();\n        },\n      ),\n    );\n  }\n\n  // 选择倍速\n  void showSetSpeedSheet() {\n    final double currentSpeed = playerController.playerSpeed;\n    KazumiDialog.show(builder: (context) {\n      return AlertDialog(\n        title: const Text('播放速度'),\n        content: StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return Wrap(\n            spacing: 8,\n            runSpacing: Utils.isDesktop() ? 8 : 0,\n            children: [\n              for (final double i in defaultPlaySpeedList) ...<Widget>[\n                if (i == currentSpeed)\n                  FilledButton(\n                    onPressed: () async {\n                      await widget.setPlaybackSpeed(i);\n                      KazumiDialog.dismiss();\n                    },\n                    child: Text(i.toString()),\n                  )\n                else\n                  FilledButton.tonal(\n                    onPressed: () async {\n                      await widget.setPlaybackSpeed(i);\n                      KazumiDialog.dismiss();\n                    },\n                    child: Text(i.toString()),\n                  ),\n              ]\n            ],\n          );\n        }),\n        actions: <Widget>[\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () async {\n              await widget.setPlaybackSpeed(1.0);\n              KazumiDialog.dismiss();\n            },\n            child: const Text('默认速度'),\n          ),\n        ],\n      );\n    });\n  }\n\n  void showForwardChange() {\n    KazumiDialog.show(builder: (context) {\n      String input = \"\";\n      return AlertDialog(\n        title: const Text('跳过秒数'),\n        content: StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return TextField(\n            inputFormatters: [\n              FilteringTextInputFormatter.digitsOnly, // 只允许输入数字\n            ],\n            decoration: InputDecoration(\n              floatingLabelBehavior:\n                  FloatingLabelBehavior.never, // 控制label的显示方式\n              labelText: playerController.buttonSkipTime.toString(),\n            ),\n            onChanged: (value) {\n              input = value;\n            },\n          );\n        }),\n        actions: <Widget>[\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () async {\n              if (input != \"\") {\n                playerController.setButtonForwardTime(int.parse(input));\n                KazumiDialog.dismiss();\n              } else {\n                KazumiDialog.dismiss();\n              }\n            },\n            child: const Text('确定'),\n          ),\n        ],\n      );\n    });\n  }\n\n\n\n  @override\n  void initState() {\n    super.initState();\n    topOffsetAnimation = Tween<Offset>(\n      begin: const Offset(0.0, -1.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    bottomOffsetAnimation = Tween<Offset>(\n      begin: const Offset(0.0, 1.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    leftOffsetAnimation = Tween<Offset>(\n      begin: const Offset(1.0, 0.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true);\n    cacheSvgIcons();\n  }\n  \n  void cacheSvgIcons() {\n    cachedDanmakuOffIcon = RepaintBoundary(\n      child: SvgPicture.asset(\n        'assets/images/danmaku_off.svg',\n        height: _danmakuIconSize,\n      ),\n    );\n\n    cachedDanmakuSettingIcon = RepaintBoundary(\n      child: SvgPicture.asset(\n        'assets/images/danmaku_setting.svg',\n        height: _danmakuIconSize,\n      ),\n    );\n  }\n  \n  Widget danmakuOnIcon(BuildContext context) {\n    final colorHex = Theme.of(context)\n        .colorScheme\n        .primary\n        .toARGB32()\n        .toRadixString(16)\n        .substring(2);\n\n    if (cachedSvgString != colorHex) {\n      cachedSvgString = colorHex;\n      final svgString = danmakuOnSvg.replaceFirst('00AEEC', colorHex);\n      cachedDanmakuOnIcon = RepaintBoundary(\n        child: SvgPicture.string(\n          svgString,\n          height: _danmakuIconSize,\n        ),\n      );\n    }\n\n    return cachedDanmakuOnIcon!;\n  }\n\n  Widget _buildDanmakuToggleButton(BuildContext context) {\n    return IconButton(\n      color: Colors.white,\n      icon: playerController.danmakuLoading\n          ? SizedBox(\n              width: _danmakuIconSize,\n              height: _danmakuIconSize,\n              child: CircularProgressIndicator(\n                strokeWidth: _loadingIndicatorStrokeWidth,\n              ),\n            )\n          : (playerController.danmakuOn\n              ? danmakuOnIcon(context)\n              : cachedDanmakuOffIcon!),\n      onPressed: playerController.danmakuLoading\n          ? null\n          : () {\n              widget.handleDanmaku();\n            },\n      tooltip: playerController.danmakuLoading\n          ? '弹幕加载中...'\n          : (playerController.danmakuOn\n              ? '关闭弹幕'\n              : '打开弹幕'),\n    );\n  }\n\n  Widget forwardIcon() {\n    return Tooltip(\n      message: '长按修改时间',\n      child: GestureDetector(\n        onLongPress: () => showForwardChange(),\n        child: IconButton(\n          icon: Image.asset(\n            'assets/images/forward_80.png',\n            color: Colors.white,\n            height: 24,\n          ),\n          onPressed: () {\n            widget.skipOP();\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Observer(builder: (context) {\n      return Stack(\n        alignment: Alignment.center,\n        children: [\n          //顶部渐变区域\n          AnimatedPositioned(\n            duration: const Duration(seconds: 1),\n            top: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? Container(\n                      height: 50,\n                      decoration: const BoxDecoration(\n                        gradient: LinearGradient(\n                          begin: Alignment.topCenter,\n                          end: Alignment.bottomCenter,\n                          colors: [\n                            Colors.black45,\n                            Colors.transparent,\n                          ],\n                        ),\n                      ),\n                    )\n                  : SlideTransition(\n                      position: topOffsetAnimation,\n                      child: Container(\n                        height: 50,\n                        decoration: const BoxDecoration(\n                          gradient: LinearGradient(\n                            begin: Alignment.topCenter,\n                            end: Alignment.bottomCenter,\n                            colors: [\n                              Colors.black45,\n                              Colors.transparent,\n                            ],\n                          ),\n                        ),\n                      ),\n                    ),\n            ),\n          ),\n\n          //底部渐变区域\n          AnimatedPositioned(\n            duration: const Duration(seconds: 1),\n            bottom: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? Container(\n                      height: 100,\n                      decoration: const BoxDecoration(\n                        gradient: LinearGradient(\n                          begin: Alignment.topCenter,\n                          end: Alignment.bottomCenter,\n                          colors: [\n                            Colors.transparent,\n                            Colors.black45,\n                          ],\n                        ),\n                      ),\n                    )\n                  : SlideTransition(\n                      position: bottomOffsetAnimation,\n                      child: Container(\n                        height: 100,\n                        decoration: const BoxDecoration(\n                          gradient: LinearGradient(\n                            begin: Alignment.topCenter,\n                            end: Alignment.bottomCenter,\n                            colors: [\n                              Colors.transparent,\n                              Colors.black45,\n                            ],\n                          ),\n                        ),\n                      ),\n                    ),\n            ),\n          ),\n          // 顶部进度条\n          Positioned(\n              top: 25,\n              child: playerController.showSeekTime\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                          padding: const EdgeInsets.all(8.0),\n                          decoration: BoxDecoration(\n                            color: Colors.black54,\n                            borderRadius: BorderRadius.circular(8.0), // 圆角\n                          ),\n                          child: Text(\n                            playerController.currentPosition.compareTo(\n                                        playerController.playerPosition) >\n                                    0\n                                ? '快进 ${playerController.currentPosition.inSeconds - playerController.playerPosition.inSeconds} 秒'\n                                : '快退 ${playerController.playerPosition.inSeconds - playerController.currentPosition.inSeconds} 秒',\n                            style: const TextStyle(\n                              color: Colors.white,\n                            ),\n                          ),\n                        ),\n                      ],\n                    )\n                  : Container()),\n          // 顶部播放速度条\n          Positioned(\n              top: 25,\n              child: playerController.showPlaySpeed\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                          padding: const EdgeInsets.all(8.0),\n                          decoration: BoxDecoration(\n                            color: Colors.black54,\n                            borderRadius: BorderRadius.circular(8.0), // 圆角\n                          ),\n                          child: const Row(\n                            children: <Widget>[\n                              Icon(Icons.fast_forward, color: Colors.white),\n                              Text(\n                                ' 倍速播放',\n                                style: TextStyle(\n                                  color: Colors.white,\n                                ),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ],\n                    )\n                  : Container()),\n          // 亮度条\n          Positioned(\n              top: 25,\n              child: playerController.showBrightness\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                            padding: const EdgeInsets.all(8.0),\n                            decoration: BoxDecoration(\n                              color: Colors.black54,\n                              borderRadius: BorderRadius.circular(8.0), // 圆角\n                            ),\n                            child: Row(\n                              children: <Widget>[\n                                const Icon(Icons.brightness_7,\n                                    color: Colors.white),\n                                Text(\n                                  ' ${(playerController.brightness * 100).toInt()} %',\n                                  style: const TextStyle(\n                                    color: Colors.white,\n                                  ),\n                                ),\n                              ],\n                            )),\n                      ],\n                    )\n                  : Container()),\n          // 音量条\n          Positioned(\n              top: 25,\n              child: playerController.showVolume\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                            padding: const EdgeInsets.all(8.0),\n                            decoration: BoxDecoration(\n                              color: Colors.black54,\n                              borderRadius: BorderRadius.circular(8.0), // 圆角\n                            ),\n                            child: Row(\n                              children: <Widget>[\n                                const Icon(Icons.volume_down,\n                                    color: Colors.white),\n                                Text(\n                                  ' ${playerController.volume.toInt()}%',\n                                  style: const TextStyle(\n                                    color: Colors.white,\n                                  ),\n                                ),\n                              ],\n                            )),\n                      ],\n                    )\n                  : Container()),\n          // 右侧锁定按钮\n          (Utils.isDesktop() || !videoPageController.isFullscreen)\n              ? Container()\n              : Positioned(\n                  right: 0,\n                  top: 0,\n                  bottom: 0,\n                  child: Visibility(\n                    visible: widget.disableAnimations\n                        ? playerController.showVideoController\n                        : true,\n                    child: widget.disableAnimations\n                        ? leftControlWidget\n                        : SlideTransition(\n                            position: leftOffsetAnimation,\n                            child: leftControlWidget),\n                  ),\n                ),\n          // 自定义顶部组件\n          Positioned(\n            top: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? topControlWidget\n                  : SlideTransition(\n                      position: topOffsetAnimation, child: topControlWidget),\n            ),\n          ),\n          // 自定义播放器底部组件\n          Positioned(\n            bottom: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? bottomControlWidget\n                  : SlideTransition(\n                      position: bottomOffsetAnimation,\n                      child: bottomControlWidget),\n            ),\n          ),\n        ],\n      );\n    });\n  }\n\n  Widget get bottomControlWidget {\n    return Observer(\n      builder: (context) {\n        return SafeArea(\n          top: false,\n          bottom: videoPageController.isFullscreen,\n          left: videoPageController.isFullscreen,\n          right: videoPageController.isFullscreen,\n          child: MouseRegion(\n            cursor: (videoPageController.isFullscreen &&\n                    !playerController.showVideoController)\n                ? SystemMouseCursors.none\n                : SystemMouseCursors.basic,\n            onEnter: (_) {\n              widget.cancelHideTimer();\n            },\n            onExit: (_) {\n              widget.cancelHideTimer();\n              widget.startHideTimer();\n            },\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                if (!Utils.isDesktop() && !Utils.isTablet())\n                  Container(\n                    padding: const EdgeInsets.only(left: 10.0, bottom: 10),\n                    child: Text(\n                      \"${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}\",\n                      style: const TextStyle(\n                        color: Colors.white,\n                        fontSize: 12.0,\n                        fontFeatures: [\n                          FontFeature.tabularFigures(),\n                        ],\n                      ),\n                    ),\n                  ),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 10),\n                  child: ProgressBar(\n                    thumbRadius: 8,\n                    thumbGlowRadius: 18,\n                    timeLabelLocation: Utils.isTablet()\n                        ? TimeLabelLocation.sides\n                        : TimeLabelLocation.none,\n                    timeLabelTextStyle: const TextStyle(\n                      color: Colors.white,\n                      fontSize: 12.0,\n                      fontFeatures: [\n                        FontFeature.tabularFigures(),\n                      ],\n                    ),\n                    progress: playerController.currentPosition,\n                    buffered: playerController.buffer,\n                    total: playerController.duration,\n                    onSeek: (duration) {\n                      playerController.seek(duration);\n                    },\n                    onDragStart: (details) {\n                      widget.handleProgressBarDragStart(details);\n                    },\n                    onDragUpdate: (details) =>\n                        {playerController.currentPosition = details.timeStamp},\n                    onDragEnd: () {\n                      widget.handleProgressBarDragEnd();\n                    },\n                  ),\n                ),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 10),\n                  child: Row(\n                    children: [\n                      IconButton(\n                        color: Colors.white,\n                        icon: Icon(playerController.playing\n                            ? Icons.pause_rounded\n                            : Icons.play_arrow_rounded),\n                        onPressed: () {\n                          playerController.playOrPause();\n                        },\n                      ),\n                      // 更换选集\n                      if (videoPageController.isFullscreen ||\n                          Utils.isTablet() ||\n                          Utils.isDesktop())\n                        IconButton(\n                          color: Colors.white,\n                          icon: const Icon(Icons.skip_next_rounded),\n                          onPressed: () => widget.handlePreNextEpisode('next'),\n                        ),\n                      if (Utils.isDesktop())\n                        Container(\n                          padding: const EdgeInsets.only(left: 10.0),\n                          child: Text(\n                            \"${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}\",\n                            style: const TextStyle(\n                              color: Colors.white,\n                              fontSize: 16.0,\n                              fontFeatures: [\n                                FontFeature.tabularFigures(),\n                              ],\n                            ),\n                          ),\n                        ),\n                      if (Utils.isDesktop())\n                        Expanded(\n                          child: LayoutBuilder(\n                            builder: (context, constraints) {\n                              bool isSpaceEnough = constraints.maxWidth > 600;\n                              return Center(\n                                child: Row(\n                                  mainAxisAlignment: MainAxisAlignment.center,\n                                  children: [\n                                    _buildDanmakuToggleButton(context),\n                                    IconButton(\n                                      onPressed: () {\n                                        widget.keyboardFocus.requestFocus();\n                                        showModalBottomSheet(\n                                            isScrollControlled: true,\n                                            constraints: BoxConstraints(\n                                                maxHeight: MediaQuery.of(context)\n                                                        .size\n                                                        .height *\n                                                    3 /\n                                                    4,\n                                                maxWidth: (Utils.isDesktop() ||\n                                                        Utils.isTablet())\n                                                    ? MediaQuery.of(context)\n                                                            .size\n                                                            .width *\n                                                        9 /\n                                                        16\n                                                    : MediaQuery.of(context)\n                                                        .size\n                                                        .width),\n                                            clipBehavior: Clip.antiAlias,\n                                            context: context,\n                                            builder: (context) {\n                                              return DanmakuSettingsSheet(\n                                                danmakuController:\n                                                    playerController\n                                                        .danmakuController,\n                                                onUpdateDanmakuSpeed:\n                                                    playerController.updateDanmakuSpeed,\n                                              );\n                                            });\n                                      },\n                                      color: Colors.white,\n                                      icon: cachedDanmakuSettingIcon!,\n                                    ),\n                                    if (isSpaceEnough) danmakuTextField,\n                                  ],\n                                ),\n                              );\n                            },\n                          ),\n                        ),\n                      if (!Utils.isDesktop()) ...[\n                        IconButton(\n                          color: Colors.white,\n                          icon: playerController.danmakuOn\n                              ? danmakuOnIcon(context)\n                              : cachedDanmakuOffIcon!,\n                          onPressed: () {\n                            widget.handleDanmaku();\n                          },\n                          tooltip:\n                              playerController.danmakuOn ? '关闭弹幕' : '打开弹幕',\n                        ),\n                        if (playerController.danmakuOn) ...[\n                          IconButton(\n                            onPressed: () {\n                              showModalBottomSheet(\n                                  isScrollControlled: true,\n                                  constraints: BoxConstraints(\n                                      maxHeight:\n                                          MediaQuery.of(context).size.height *\n                                              3 /\n                                              4,\n                                      maxWidth:\n                                          (Utils.isDesktop() || Utils.isTablet())\n                                              ? MediaQuery.of(context).size.width *\n                                                  9 /\n                                                  16\n                                              : MediaQuery.of(context).size.width),\n                                  clipBehavior: Clip.antiAlias,\n                                  context: context,\n                                  builder: (context) {\n                                    return DanmakuSettingsSheet(\n                                      danmakuController:\n                                          playerController.danmakuController,\n                                      onUpdateDanmakuSpeed:\n                                          playerController.updateDanmakuSpeed,\n                                    );\n                                  });\n                            },\n                            color: Colors.white,\n                            icon: cachedDanmakuSettingIcon!,\n                          ),\n                          Expanded(child: danmakuTextField),\n                        ],\n                        if (!playerController.danmakuOn) const Spacer(),\n                      ],\n                      // 超分辨率\n                      MenuAnchor(\n                        consumeOutsideTap: true,\n                        onOpen: () {\n                          widget.cancelHideTimer();\n                          playerController.canHidePlayerPanel = false;\n                        },\n                        onClose: () {\n                          widget.cancelHideTimer();\n                          widget.startHideTimer();\n                          playerController.canHidePlayerPanel = true;\n                        },\n                        builder: (BuildContext context, MenuController controller,\n                            Widget? child) {\n                          return TextButton(\n                            onPressed: () {\n                              if (controller.isOpen) {\n                                controller.close();\n                              } else {\n                                controller.open();\n                              }\n                            },\n                            child: const Text(\n                              '超分辨率',\n                              style: TextStyle(color: Colors.white),\n                            ),\n                          );\n                        },\n                        menuChildren: List<MenuItemButton>.generate(\n                          3,\n                          (int index) => MenuItemButton(\n                            onPressed: () =>\n                                widget.handleSuperResolutionChange(index + 1),\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\n                                  index + 1 == 1\n                                      ? '关闭'\n                                      : index + 1 == 2\n                                          ? '效率档'\n                                          : '质量档',\n                                  style: TextStyle(\n                                    color: playerController.superResolutionType ==\n                                            index + 1\n                                        ? Theme.of(context).colorScheme.primary\n                                        : null,\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                      ),\n                      // 倍速播放\n                      MenuAnchor(\n                        consumeOutsideTap: true,\n                        onOpen: () {\n                          widget.cancelHideTimer();\n                          playerController.canHidePlayerPanel = false;\n                        },\n                        onClose: () {\n                          widget.cancelHideTimer();\n                          widget.startHideTimer();\n                          playerController.canHidePlayerPanel = true;\n                        },\n                        builder: (BuildContext context, MenuController controller,\n                            Widget? child) {\n                          return TextButton(\n                            onPressed: () {\n                              if (controller.isOpen) {\n                                controller.close();\n                              } else {\n                                controller.open();\n                              }\n                            },\n                            child: Text(\n                              playerController.playerSpeed == 1.0\n                                  ? '倍速'\n                                  : '${playerController.playerSpeed}x',\n                              style: const TextStyle(color: Colors.white),\n                            ),\n                          );\n                        },\n                        menuChildren: [\n                          for (final double i\n                              in defaultPlaySpeedList) ...<MenuItemButton>[\n                            MenuItemButton(\n                              onPressed: () async {\n                                await widget.setPlaybackSpeed(i);\n                              },\n                              child: Container(\n                                height: 48,\n                                constraints: BoxConstraints(minWidth: 112),\n                                child: Align(\n                                  alignment: Alignment.centerLeft,\n                                  child: Text(\n                                    '${i}x',\n                                    style: TextStyle(\n                                      color: i == playerController.playerSpeed\n                                          ? Theme.of(context).colorScheme.primary\n                                          : null,\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ],\n                        ],\n                      ),\n                      MenuAnchor(\n                        consumeOutsideTap: true,\n                        onOpen: () {\n                          widget.cancelHideTimer();\n                          playerController.canHidePlayerPanel = false;\n                        },\n                        onClose: () {\n                          widget.cancelHideTimer();\n                          widget.startHideTimer();\n                          playerController.canHidePlayerPanel = true;\n                        },\n                        builder: (BuildContext context, MenuController controller,\n                            Widget? child) {\n                          return IconButton(\n                            onPressed: () {\n                              if (controller.isOpen) {\n                                controller.close();\n                              } else {\n                                controller.open();\n                              }\n                            },\n                            icon: const Icon(\n                              Icons.aspect_ratio_rounded,\n                              color: Colors.white,\n                            ),\n                            tooltip: '视频比例',\n                          );\n                        },\n                        menuChildren: [\n                          for (final entry in aspectRatioTypeMap.entries)\n                            MenuItemButton(\n                              onPressed: () =>\n                                  playerController.aspectRatioType = entry.key,\n                              child: Container(\n                                height: 48,\n                                constraints: BoxConstraints(minWidth: 112),\n                                child: Align(\n                                  alignment: Alignment.centerLeft,\n                                  child: Text(\n                                    entry.value,\n                                    style: TextStyle(\n                                      color: entry.key ==\n                                              playerController.aspectRatioType\n                                          ? Theme.of(context).colorScheme.primary\n                                          : null,\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ),\n                        ],\n                      ),\n                      (!videoPageController.isFullscreen &&\n                              !Utils.isTablet() &&\n                              !Utils.isDesktop())\n                          ? Container()\n                          : IconButton(\n                              color: Colors.white,\n                              icon: const Icon(Icons.menu_open_rounded),\n                              onPressed: () {\n                                videoPageController.showTabBody =\n                                    !videoPageController.showTabBody;\n                                widget.openMenu();\n                              },\n                            ),\n                      (Utils.isTablet() &&\n                              videoPageController.isFullscreen &&\n                              MediaQuery.of(context).size.height <\n                                  MediaQuery.of(context).size.width)\n                          ? Container()\n                          : IconButton(\n                              color: Colors.white,\n                              icon: Icon(videoPageController.isFullscreen\n                                  ? Icons.fullscreen_exit_rounded\n                                  : Icons.fullscreen_rounded),\n                              onPressed: () {\n                                widget.handleFullscreen();\n                              },\n                            ),\n                    ],\n                  ),\n                ),\n                if (Utils.isTablet() || Utils.isDesktop())\n                  const SizedBox(height: 6),\n              ],\n            ),\n          ),\n        );\n      }\n    );\n  }\n\n  Widget get topControlWidget {\n    return Observer(\n      builder: (context) {\n        return EmbeddedNativeControlArea(\n          requireOffset: !videoPageController.isFullscreen,\n          child: SafeArea(\n            top: false,\n            bottom: false,\n            left: videoPageController.isFullscreen,\n            right: videoPageController.isFullscreen,\n            child: MouseRegion(\n              cursor: (videoPageController.isFullscreen &&\n                      !playerController.showVideoController)\n                  ? SystemMouseCursors.none\n                  : SystemMouseCursors.basic,\n              onEnter: (_) {\n                widget.cancelHideTimer();\n              },\n              onExit: (_) {\n                widget.cancelHideTimer();\n                widget.startHideTimer();\n              },\n              child: Row(\n                children: [\n                  IconButton(\n                    color: Colors.white,\n                    icon: const Icon(Icons.arrow_back_rounded),\n                    onPressed: () {\n                      widget.onBackPressed(context);\n                    },\n                  ),\n                  // 拖动条\n                  Expanded(\n                    child: dtb.DragToMoveArea(\n                      child: Text(\n                        ' ${videoPageController.title} [${videoPageController.roadList[videoPageController.currentRoad].identifier[videoPageController.currentEpisode - 1]}]',\n                        style: TextStyle(\n                          color: Colors.white,\n                          fontSize:\n                              Theme.of(context).textTheme.titleMedium!.fontSize,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    ),\n                  ),\n                  // 跳过\n                  forwardIcon(),\n                  if (Utils.isDesktop() && !videoPageController.isFullscreen)\n                    IconButton(\n                      onPressed: () {\n                        if (videoPageController.isPip) {\n                          Utils.exitDesktopPIPWindow();\n                        } else {\n                          Utils.enterDesktopPIPWindow();\n                        }\n                        videoPageController.isPip = !videoPageController.isPip;\n                      },\n                      icon: const Icon(\n                        Icons.picture_in_picture,\n                        color: Colors.white,\n                      ),\n                    ),\n                  // 追番\n                  CollectButton(\n                    bangumiItem: videoPageController.bangumiItem,\n                    onOpen: () {\n                      widget.cancelHideTimer();\n                      playerController.canHidePlayerPanel = false;\n                    },\n                    onClose: () {\n                      widget.cancelHideTimer();\n                      widget.startHideTimer();\n                      playerController.canHidePlayerPanel = true;\n                    },\n                  ),\n                  MenuAnchor(\n                    consumeOutsideTap: true,\n                    onOpen: () {\n                      widget.cancelHideTimer();\n                      playerController.canHidePlayerPanel = false;\n                    },\n                    onClose: () {\n                      widget.cancelHideTimer();\n                      widget.startHideTimer();\n                      playerController.canHidePlayerPanel = true;\n                    },\n                    builder: (BuildContext context, MenuController controller,\n                        Widget? child) {\n                      return IconButton(\n                        onPressed: () {\n                          if (controller.isOpen) {\n                            controller.close();\n                          } else {\n                            controller.open();\n                          }\n                        },\n                        icon: const Icon(\n                          Icons.more_vert,\n                          color: Colors.white,\n                        ),\n                      );\n                    },\n                    menuChildren: [\n                      MenuItemButton(\n                        onPressed: () {\n                          widget.showDanmakuSwitch();\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\"弹幕切换\"),\n                          ),\n                        ),\n                      ),\n                      MenuItemButton(\n                        onPressed: () {\n                          widget.showVideoInfo();\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\"视频详情\"),\n                          ),\n                        ),\n                      ),\n                      MenuItemButton(\n                        onPressed: () {\n                          bool needRestart = playerController.playing;\n                          playerController.pause();\n                          RemotePlay()\n                              .castVideo(playerController.videoUrl,\n                                  videoPageController.currentPlugin.referer)\n                              .whenComplete(() {\n                            if (needRestart) {\n                              playerController.play();\n                            }\n                          });\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\"远程投屏\"),\n                          ),\n                        ),\n                      ),\n                      MenuItemButton(\n                        onPressed: () {\n                          playerController.lanunchExternalPlayer();\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\"外部播放\"),\n                          ),\n                        ),\n                      ),\n                      // 定时关闭\n                      SubmenuButton(\n                        menuChildren: [\n                          MenuItemButton(\n                            onPressed: () {\n                              TimedShutdownService().cancel();\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\n                                  \"不开启\",\n                                  style: TextStyle(\n                                    color: !TimedShutdownService().isActive\n                                        ? Theme.of(context).colorScheme.primary\n                                        : null,\n                                  ),\n                                ),\n                              ),\n                            ),\n                          ),\n                          for (final int minutes in [15, 30, 60])\n                            MenuItemButton(\n                              onPressed: () {\n                                TimedShutdownService().start(minutes, onExpired: widget.pauseForTimedShutdown);\n                                KazumiDialog.showToast(message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭');\n                              },\n                              child: Container(\n                                height: 48,\n                                constraints: BoxConstraints(minWidth: 112),\n                                child: Align(\n                                  alignment: Alignment.centerLeft,\n                                  child: Text(\n                                    \"$minutes 分钟\",\n                                    style: TextStyle(\n                                      color: TimedShutdownService().setMinutes == minutes\n                                          ? Theme.of(context).colorScheme.primary\n                                          : null,\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ),\n                          MenuItemButton(\n                            onPressed: () {\n                              TimedShutdownService.showCustomTimerDialog(\n                                onExpired: widget.pauseForTimedShutdown,\n                              );\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\"自定义\"),\n                              ),\n                            ),\n                          ),\n                        ],\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: ValueListenableBuilder<int>(\n                              valueListenable: TimedShutdownService().remainingSecondsNotifier,\n                              builder: (context, remainingSeconds, child) {\n                                return Text(\n                                  remainingSeconds > 0\n                                      ? \"定时关闭 (${TimedShutdownService().formatRemainingTime()})\"\n                                      : \"定时关闭\",\n                                );\n                              },\n                            ),\n                          ),\n                        ),\n                      ),\n                      SubmenuButton(\n                        menuChildren: [\n                          MenuItemButton(\n                            child: Container(\n\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\n                                    \"当前房间: ${playerController.syncplayRoom == '' ? '未加入' : playerController.syncplayRoom}\"),\n                              ),\n                            ),\n                          ),\n                          MenuItemButton(\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\n                                    \"网络延时: ${playerController.syncplayClientRtt}ms\"),\n                              ),\n                            ),\n                          ),\n                          MenuItemButton(\n                            onPressed: () {\n                              widget.showSyncPlayRoomCreateDialog();\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\"加入房间\"),\n                              ),\n                            ),\n                          ),\n                          MenuItemButton(\n                            onPressed: () {\n                              widget.showSyncPlayEndPointSwitchDialog();\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\"切换服务器\"),\n                              ),\n                            ),\n                          ),\n                          MenuItemButton(\n                            onPressed: () async {\n                              await playerController.exitSyncPlayRoom();\n                            },\n                            child: Container(\n                              height: 48,\n                              constraints: BoxConstraints(minWidth: 112),\n                              child: Align(\n                                alignment: Alignment.centerLeft,\n                                child: Text(\"断开连接\"),\n                              ),\n                            ),\n                          ),\n                        ],\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\"一起看\"),\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n      }\n    );\n  }\n\n  Widget get leftControlWidget {\n    return Observer(\n      builder: (context) {\n        return SafeArea(\n          top: false,\n          bottom: false,\n          left: videoPageController.isFullscreen,\n          right: videoPageController.isFullscreen,\n          child: Column(\n            children: [\n              const Spacer(),\n              (playerController.lockPanel)\n                  ? Container()\n                  : IconButton(\n                      icon: const Icon(\n                        Icons.photo_camera_outlined,\n                        color: Colors.white,\n                      ),\n                      onPressed: () {\n                        widget.handleScreenShot();\n                      },\n                    ),\n              IconButton(\n                icon: Icon(\n                  playerController.lockPanel ? Icons.lock_outline : Icons.lock_open,\n                  color: Colors.white,\n                ),\n                onPressed: () {\n                  playerController.lockPanel = !playerController.lockPanel;\n                },\n              ),\n              const Spacer(),\n            ],\n          ),\n        );\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/player_item_surface.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:media_kit_video/media_kit_video.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\n\nclass PlayerItemSurface extends StatefulWidget {\n  const PlayerItemSurface({super.key});\n\n  @override\n  State<PlayerItemSurface> createState() => _PlayerItemSurfaceState();\n}\n\nclass _PlayerItemSurfaceState extends State<PlayerItemSurface> {\n  final PlayerController playerController = Modular.get<PlayerController>();\n\n  @override\n  Widget build(BuildContext context) {\n    return Observer(builder: (context) {\n      if (playerController.loading ||\n          playerController.videoController == null) {\n        return Container(\n          color: Colors.black,\n          child: const Center(\n            child: CircularProgressIndicator(),\n          ),\n        );\n      }\n\n      return Video(\n        controller: playerController.videoController!,\n        controls: NoVideoControls,\n        fit: playerController.aspectRatioType == 1\n            ? BoxFit.contain\n            : playerController.aspectRatioType == 2\n                ? BoxFit.cover\n                : BoxFit.fill,\n        subtitleViewConfiguration: SubtitleViewConfiguration(\n          style: TextStyle(\n            color: Colors.pink,\n            fontSize: 48.0,\n            background: Paint()..color = Colors.transparent,\n            decoration: TextDecoration.none,\n            fontWeight: FontWeight.bold,\n            shadows: const [\n              Shadow(\n                offset: Offset(1.0, 1.0),\n                blurRadius: 3.0,\n                color: Color.fromARGB(255, 255, 255, 255),\n              ),\n              Shadow(\n                offset: Offset(-1.0, -1.0),\n                blurRadius: 3.0,\n                color: Color.fromARGB(125, 255, 255, 255),\n              ),\n            ],\n          ),\n          textAlign: TextAlign.center,\n          padding: const EdgeInsets.all(24.0),\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/player/smallest_player_item_panel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/utils/remote.dart';\nimport 'package:kazumi/pages/settings/danmaku/danmaku_settings_sheet.dart';\nimport 'package:kazumi/bean/widget/collect_button.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb;\nimport 'package:audio_video_progress_bar/audio_video_progress_bar.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/utils/timed_shutdown_service.dart';\n\nclass SmallestPlayerItemPanel extends StatefulWidget {\n  const SmallestPlayerItemPanel({\n    super.key,\n    required this.onBackPressed,\n    required this.setPlaybackSpeed,\n    required this.showDanmakuSwitch,\n    required this.handleFullscreen,\n    required this.handleProgressBarDragStart,\n    required this.handleProgressBarDragEnd,\n    required this.handleSuperResolutionChange,\n    required this.animationController,\n    required this.keyboardFocus,\n    required this.handleHove,\n    required this.startHideTimer,\n    required this.cancelHideTimer,\n    required this.handleDanmaku,\n    required this.skipOP,\n    required this.showVideoInfo,\n    required this.showSyncPlayRoomCreateDialog,\n    required this.showSyncPlayEndPointSwitchDialog,\n    required this.pauseForTimedShutdown,\n    this.disableAnimations = false,\n  });\n\n  final void Function(BuildContext) onBackPressed;\n  final Future<void> Function(double) setPlaybackSpeed;\n  final void Function() showDanmakuSwitch;\n  final void Function() handleDanmaku;\n  final void Function() skipOP;\n  final void Function() handleFullscreen;\n  final void Function(ThumbDragDetails details) handleProgressBarDragStart;\n  final void Function() handleProgressBarDragEnd;\n  final Future<void> Function(int shaderIndex) handleSuperResolutionChange;\n  final void Function() handleHove;\n  final AnimationController animationController;\n  final FocusNode keyboardFocus;\n  final void Function() startHideTimer;\n  final void Function() cancelHideTimer;\n  final void Function() showVideoInfo;\n  final void Function() showSyncPlayRoomCreateDialog;\n  final void Function() showSyncPlayEndPointSwitchDialog;\n  final VoidCallback pauseForTimedShutdown;\n  final bool disableAnimations;\n\n  @override\n  State<SmallestPlayerItemPanel> createState() =>\n      _SmallestPlayerItemPanelState();\n}\n\nclass _SmallestPlayerItemPanelState extends State<SmallestPlayerItemPanel> {\n  Box setting = GStorage.setting;\n  late bool haEnable;\n  late Animation<Offset> topOffsetAnimation;\n  late Animation<Offset> bottomOffsetAnimation;\n  late Animation<Offset> leftOffsetAnimation;\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final PlayerController playerController = Modular.get<PlayerController>();\n  final TextEditingController textController = TextEditingController();\n  \n  // SVG Caches\n  String? cachedSvgString;\n  Widget? cachedDanmakuOnIcon;\n  Widget? cachedDanmakuOffIcon;\n\n  static const double _danmakuIconSize = 24.0;\n  static const double _loadingIndicatorStrokeWidth = 2.0;\n\n  void showForwardChange() {\n    KazumiDialog.show(builder: (context) {\n      String input = \"\";\n      return AlertDialog(\n        title: const Text('跳过秒数'),\n        content: StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return TextField(\n            inputFormatters: [\n              FilteringTextInputFormatter.digitsOnly, // 只允许输入数字\n            ],\n            decoration: InputDecoration(\n              floatingLabelBehavior:\n                  FloatingLabelBehavior.never, // 控制label的显示方式\n              labelText: playerController.buttonSkipTime.toString(),\n            ),\n            onChanged: (value) {\n              input = value;\n            },\n          );\n        }),\n        actions: <Widget>[\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () async {\n              if (input != \"\") {\n                playerController.setButtonForwardTime(int.parse(input));\n                KazumiDialog.dismiss();\n              } else {\n                KazumiDialog.dismiss();\n              }\n            },\n            child: const Text('确定'),\n          ),\n        ],\n      );\n    });\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    topOffsetAnimation = Tween<Offset>(\n      begin: const Offset(0.0, -1.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    bottomOffsetAnimation = Tween<Offset>(\n      begin: const Offset(0.0, 1.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    leftOffsetAnimation = Tween<Offset>(\n      begin: const Offset(1.0, 0.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: widget.animationController,\n      curve: Curves.easeInOut,\n    ));\n    haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true);\n    cacheSvgIcons();\n  }\n  \n  void cacheSvgIcons() {\n    cachedDanmakuOffIcon = RepaintBoundary(\n      child: SvgPicture.asset(\n        'assets/images/danmaku_off.svg',\n        height: _danmakuIconSize,\n      ),\n    );\n  }\n  \n  Widget danmakuOnIcon(BuildContext context) {\n    final colorHex = Theme.of(context)\n        .colorScheme\n        .primary\n        .toARGB32()\n        .toRadixString(16)\n        .substring(2);\n\n    if (cachedSvgString != colorHex) {\n      cachedSvgString = colorHex;\n      final svgString = danmakuOnSvg.replaceFirst('00AEEC', colorHex);\n      cachedDanmakuOnIcon = RepaintBoundary(\n        child: SvgPicture.string(\n          svgString,\n          height: _danmakuIconSize,\n        ),\n      );\n    }\n\n    return cachedDanmakuOnIcon!;\n  }\n\n  Widget _buildDanmakuToggleButton(BuildContext context) {\n    return IconButton(\n      color: Colors.white,\n      icon: playerController.danmakuLoading\n          ? SizedBox(\n              width: _danmakuIconSize,\n              height: _danmakuIconSize,\n              child: CircularProgressIndicator(\n                strokeWidth: _loadingIndicatorStrokeWidth,\n              ),\n            )\n          : (playerController.danmakuOn\n              ? danmakuOnIcon(context)\n              : cachedDanmakuOffIcon!),\n      onPressed: playerController.danmakuLoading\n          ? null\n          : () {\n              widget.handleDanmaku();\n            },\n      tooltip: playerController.danmakuLoading\n          ? '弹幕加载中...'\n          : (playerController.danmakuOn\n              ? '关闭弹幕'\n              : '打开弹幕'),\n    );\n  }\n\n  Widget forwardIcon() {\n    return Tooltip(\n      message: '长按修改时间',\n      child: GestureDetector(\n        onLongPress: () => showForwardChange(),\n        child: IconButton(\n          icon: Image.asset(\n            'assets/images/forward_80.png',\n            color: Colors.white,\n            height: 24,\n          ),\n          onPressed: () {\n            widget.skipOP();\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Observer(builder: (context) {\n      return Stack(\n        alignment: Alignment.center,\n        children: [\n          //顶部渐变区域\n          AnimatedPositioned(\n            duration: const Duration(seconds: 1),\n            top: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? Container(\n                      height: 50,\n                      decoration: const BoxDecoration(\n                        gradient: LinearGradient(\n                          begin: Alignment.topCenter,\n                          end: Alignment.bottomCenter,\n                          colors: [\n                            Colors.black45,\n                            Colors.transparent,\n                          ],\n                        ),\n                      ),\n                    )\n                  : SlideTransition(\n                      position: topOffsetAnimation,\n                      child: Container(\n                        height: 50,\n                        decoration: const BoxDecoration(\n                          gradient: LinearGradient(\n                            begin: Alignment.topCenter,\n                            end: Alignment.bottomCenter,\n                            colors: [\n                              Colors.black45,\n                              Colors.transparent,\n                            ],\n                          ),\n                        ),\n                      ),\n                    ),\n            ),\n          ),\n\n          //底部渐变区域\n          AnimatedPositioned(\n            duration: const Duration(seconds: 1),\n            bottom: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? Container(\n                      height: 100,\n                      decoration: const BoxDecoration(\n                        gradient: LinearGradient(\n                          begin: Alignment.topCenter,\n                          end: Alignment.bottomCenter,\n                          colors: [\n                            Colors.transparent,\n                            Colors.black45,\n                          ],\n                        ),\n                      ),\n                    )\n                  : SlideTransition(\n                      position: bottomOffsetAnimation,\n                      child: Container(\n                        height: 100,\n                        decoration: const BoxDecoration(\n                          gradient: LinearGradient(\n                            begin: Alignment.topCenter,\n                            end: Alignment.bottomCenter,\n                            colors: [\n                              Colors.transparent,\n                              Colors.black45,\n                            ],\n                          ),\n                        ),\n                      ),\n                    ),\n            ),\n          ),\n          // 顶部进度条\n          Positioned(\n              top: 25,\n              child: playerController.showSeekTime\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                          padding: const EdgeInsets.all(8.0),\n                          decoration: BoxDecoration(\n                            color: Colors.black54,\n                            borderRadius: BorderRadius.circular(8.0), // 圆角\n                          ),\n                          child: Text(\n                            playerController.currentPosition.compareTo(\n                                        playerController.playerPosition) >\n                                    0\n                                ? '快进 ${playerController.currentPosition.inSeconds - playerController.playerPosition.inSeconds} 秒'\n                                : '快退 ${playerController.playerPosition.inSeconds - playerController.currentPosition.inSeconds} 秒',\n                            style: const TextStyle(\n                              color: Colors.white,\n                            ),\n                          ),\n                        ),\n                      ],\n                    )\n                  : Container()),\n          // 顶部播放速度条\n          Positioned(\n              top: 25,\n              child: playerController.showPlaySpeed\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                          padding: const EdgeInsets.all(8.0),\n                          decoration: BoxDecoration(\n                            color: Colors.black54,\n                            borderRadius: BorderRadius.circular(8.0), // 圆角\n                          ),\n                          child: const Row(\n                            children: <Widget>[\n                              Icon(Icons.fast_forward, color: Colors.white),\n                              Text(\n                                ' 倍速播放',\n                                style: TextStyle(\n                                  color: Colors.white,\n                                ),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ],\n                    )\n                  : Container()),\n          // 亮度条\n          Positioned(\n              top: 25,\n              child: playerController.showBrightness\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                            padding: const EdgeInsets.all(8.0),\n                            decoration: BoxDecoration(\n                              color: Colors.black54,\n                              borderRadius: BorderRadius.circular(8.0), // 圆角\n                            ),\n                            child: Row(\n                              children: <Widget>[\n                                const Icon(Icons.brightness_7,\n                                    color: Colors.white),\n                                Text(\n                                  ' ${(playerController.brightness * 100).toInt()} %',\n                                  style: const TextStyle(\n                                    color: Colors.white,\n                                  ),\n                                ),\n                              ],\n                            )),\n                      ],\n                    )\n                  : Container()),\n          // 音量条\n          Positioned(\n              top: 25,\n              child: playerController.showVolume\n                  ? Wrap(\n                      alignment: WrapAlignment.center,\n                      children: <Widget>[\n                        Container(\n                            padding: const EdgeInsets.all(8.0),\n                            decoration: BoxDecoration(\n                              color: Colors.black54,\n                              borderRadius: BorderRadius.circular(8.0), // 圆角\n                            ),\n                            child: Row(\n                              children: <Widget>[\n                                const Icon(Icons.volume_down,\n                                    color: Colors.white),\n                                Text(\n                                  ' ${playerController.volume.toInt()}%',\n                                  style: const TextStyle(\n                                    color: Colors.white,\n                                  ),\n                                ),\n                              ],\n                            )),\n                      ],\n                    )\n                  : Container()),\n          // 自定义顶部组件\n          Positioned(\n            top: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? topControlWidget\n                  : SlideTransition(\n                      position: topOffsetAnimation, child: topControlWidget),\n            ),\n          ),\n          // 自定义播放器底部组件\n          Positioned(\n            bottom: 0,\n            left: 0,\n            right: 0,\n            child: Visibility(\n              visible: !playerController.lockPanel &&\n                  (widget.disableAnimations\n                      ? playerController.showVideoController\n                      : true),\n              child: widget.disableAnimations\n                  ? bottomControlWidget\n                  : SlideTransition(\n                      position: bottomOffsetAnimation,\n                      child: bottomControlWidget),\n            ),\n          ),\n        ],\n      );\n    });\n  }\n\n  Widget get bottomControlWidget {\n    return Observer(builder: (context) {\n      return Row(\n        children: [\n          IconButton(\n            color: Colors.white,\n            icon: Icon(playerController.playing\n                ? Icons.pause_rounded\n                : Icons.play_arrow_rounded),\n            onPressed: () {\n              playerController.playOrPause();\n            },\n          ),\n          Expanded(\n            child: ProgressBar(\n              thumbRadius: 8,\n              thumbGlowRadius: 18,\n              timeLabelLocation: TimeLabelLocation.none,\n              progress: playerController.currentPosition,\n              buffered: playerController.buffer,\n              total: playerController.duration,\n              onSeek: (duration) {\n                playerController.seek(duration);\n              },\n              onDragStart: (details) {\n                widget.handleProgressBarDragStart(details);\n              },\n              onDragUpdate: (details) =>\n                  {playerController.currentPosition = details.timeStamp},\n              onDragEnd: () {\n                widget.handleProgressBarDragEnd();\n              },\n            ),\n          ),\n          Text(\n            \"    ${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}\",\n            style: const TextStyle(\n              color: Colors.white,\n              fontSize: 12.0,\n              fontFeatures: [\n                FontFeature.tabularFigures(),\n              ],\n            ),\n          ),\n          (!videoPageController.isPip)\n              ? IconButton(\n                  color: Colors.white,\n                  icon: Icon(videoPageController.isFullscreen\n                      ? Icons.fullscreen_exit_rounded\n                      : Icons.fullscreen_rounded),\n                  onPressed: () {\n                    widget.handleFullscreen();\n                  },\n                )\n              : const Text('    '),\n        ],\n      );\n    });\n  }\n\n  Widget get topControlWidget {\n    return Observer(builder: (context) {\n      return EmbeddedNativeControlArea(\n        child: Row(\n          children: [\n            IconButton(\n              color: Colors.white,\n              icon: const Icon(Icons.arrow_back_rounded),\n              onPressed: () {\n                widget.onBackPressed(context);\n              },\n            ),\n            // 拖动条\n            const Expanded(\n              child: dtb.DragToMoveArea(child: SizedBox(height: 40)),\n            ),\n            // 跳过\n            forwardIcon(),\n            if (Utils.isDesktop())\n              IconButton(\n                  onPressed: () {\n                    if (videoPageController.isPip) {\n                      Utils.exitDesktopPIPWindow();\n                    } else {\n                      Utils.enterDesktopPIPWindow();\n                    }\n                    videoPageController.isPip = !videoPageController.isPip;\n                  },\n                  icon: const Icon(Icons.picture_in_picture,\n                      color: Colors.white)),\n            // 弹幕开关\n            _buildDanmakuToggleButton(context),\n            // 追番\n            CollectButton(\n              bangumiItem: videoPageController.bangumiItem,\n              onOpen: () {\n                widget.cancelHideTimer();\n                playerController.canHidePlayerPanel = false;\n              },\n              onClose: () {\n                widget.cancelHideTimer();\n                widget.startHideTimer();\n                playerController.canHidePlayerPanel = true;\n              },\n            ),\n            MenuAnchor(\n              consumeOutsideTap: true,\n              onOpen: () {\n                widget.cancelHideTimer();\n                playerController.canHidePlayerPanel = false;\n              },\n              onClose: () {\n                widget.cancelHideTimer();\n                widget.startHideTimer();\n                playerController.canHidePlayerPanel = true;\n              },\n              builder: (BuildContext context, MenuController controller,\n                  Widget? child) {\n                return IconButton(\n                  onPressed: () {\n                    if (controller.isOpen) {\n                      controller.close();\n                    } else {\n                      controller.open();\n                    }\n                  },\n                  icon: const Icon(\n                    Icons.more_vert,\n                    color: Colors.white,\n                  ),\n                );\n              },\n              menuChildren: [\n                SubmenuButton(\n                  menuChildren: List<MenuItemButton>.generate(\n                    3,\n                    (int index) => MenuItemButton(\n                      onPressed: () =>\n                          playerController.aspectRatioType = index + 1,\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                            index + 1 == 1\n                                ? '自动'\n                                : index + 1 == 2\n                                    ? '裁切填充'\n                                    : '拉伸填充',\n                            style: TextStyle(\n                                color: index + 1 ==\n                                        playerController.aspectRatioType\n                                    ? Theme.of(context).colorScheme.primary\n                                    : null),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"视频比例\"),\n                    ),\n                  ),\n                ),\n                SubmenuButton(\n                  menuChildren: [\n                    for (final double i\n                        in defaultPlaySpeedList) ...<MenuItemButton>[\n                      MenuItemButton(\n                        onPressed: () async {\n                          await widget.setPlaybackSpeed(i);\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\n                              '${i}x',\n                              style: TextStyle(\n                                  color: i == playerController.playerSpeed\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null),\n                            ),\n                          ),\n                        ),\n                      ),\n                    ],\n                  ],\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"倍速\"),\n                    ),\n                  ),\n                ),\n                SubmenuButton(\n                  menuChildren: List<MenuItemButton>.generate(\n                    3,\n                    (int index) => MenuItemButton(\n                      onPressed: () =>\n                          widget.handleSuperResolutionChange(index + 1),\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                            index + 1 == 1\n                                ? '关闭'\n                                : index + 1 == 2\n                                    ? '效率档'\n                                    : '质量档',\n                            style: TextStyle(\n                              color: playerController.superResolutionType ==\n                                      index + 1\n                                  ? Theme.of(context).colorScheme.primary\n                                  : null,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"超分辨率\"),\n                    ),\n                  ),\n                ),\n                SubmenuButton(\n                  menuChildren: [\n                    MenuItemButton(\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                              \"当前房间: ${playerController.syncplayRoom == '' ? '未加入' : playerController.syncplayRoom}\"),\n                        ),\n                      ),\n                    ),\n                    MenuItemButton(\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                              \"网络延时: ${playerController.syncplayClientRtt}ms\"),\n                        ),\n                      ),\n                    ),\n                    MenuItemButton(\n                      onPressed: () {\n                        widget.showSyncPlayRoomCreateDialog();\n                      },\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\"加入房间\"),\n                        ),\n                      ),\n                    ),\n                    MenuItemButton(\n                      onPressed: () {\n                        widget.showSyncPlayEndPointSwitchDialog();\n                      },\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\"切换服务器\"),\n                        ),\n                      ),\n                    ),\n                    MenuItemButton(\n                      onPressed: () async {\n                        await playerController.exitSyncPlayRoom();\n                      },\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\"断开连接\"),\n                        ),\n                      ),\n                    ),\n                  ],\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"一起看\"),\n                    ),\n                  ),\n                ),\n                MenuItemButton(\n                  onPressed: () {\n                    widget.showDanmakuSwitch();\n                  },\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"弹幕切换\"),\n                    ),\n                  ),\n                ),\n                MenuItemButton(\n                  onPressed: () {\n                    showModalBottomSheet(\n                      isScrollControlled: true,\n                      constraints: BoxConstraints(\n                          maxHeight: MediaQuery.of(context).size.height * 3 / 4,\n                          maxWidth: (Utils.isDesktop() || Utils.isTablet())\n                              ? MediaQuery.of(context).size.width * 9 / 16\n                              : MediaQuery.of(context).size.width),\n                      clipBehavior: Clip.antiAlias,\n                      context: context,\n                      builder: (context) {\n                        return DanmakuSettingsSheet(\n                          danmakuController:\n                              playerController.danmakuController,\n                          onUpdateDanmakuSpeed:\n                              playerController.updateDanmakuSpeed,\n                        );\n                      },\n                    );\n                  },\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"弹幕设置\"),\n                    ),\n                  ),\n                ),\n                MenuItemButton(\n                  onPressed: () {\n                    widget.showVideoInfo();\n                  },\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"视频详情\"),\n                    ),\n                  ),\n                ),\n                MenuItemButton(\n                  onPressed: () {\n                    bool needRestart = playerController.playing;\n                    playerController.pause();\n                    RemotePlay()\n                        .castVideo(playerController.videoUrl,\n                            videoPageController.currentPlugin.referer)\n                        .whenComplete(() {\n                      if (needRestart) {\n                        playerController.play();\n                      }\n                    });\n                  },\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"远程投屏\"),\n                    ),\n                  ),\n                ),\n                MenuItemButton(\n                  onPressed: () {\n                    playerController.lanunchExternalPlayer();\n                  },\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: Text(\"外部播放\"),\n                    ),\n                  ),\n                ),\n                // 定时关闭\n                SubmenuButton(\n                  menuChildren: [\n                    MenuItemButton(\n                      onPressed: () {\n                        TimedShutdownService().cancel();\n                      },\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                            \"不开启\",\n                            style: TextStyle(\n                              color: !TimedShutdownService().isActive\n                                  ? Theme.of(context).colorScheme.primary\n                                  : null,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                    for (final int minutes in [15, 30, 60])\n                      MenuItemButton(\n                        onPressed: () {\n                          TimedShutdownService().start(minutes, onExpired: widget.pauseForTimedShutdown);\n                          KazumiDialog.showToast(message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭');\n                        },\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\n                              \"$minutes 分钟\",\n                              style: TextStyle(\n                                color: TimedShutdownService().setMinutes == minutes\n                                    ? Theme.of(context).colorScheme.primary\n                                    : null,\n                              ),\n                            ),\n                          ),\n                        ),\n                      ),\n                    MenuItemButton(\n                      onPressed: () {\n                        TimedShutdownService.showCustomTimerDialog(\n                          onExpired: widget.pauseForTimedShutdown,\n                        );\n                      },\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\"自定义\"),\n                        ),\n                      ),\n                    ),\n                  ],\n                  child: Container(\n                    height: 48,\n                    constraints: BoxConstraints(minWidth: 112),\n                    child: Align(\n                      alignment: Alignment.centerLeft,\n                      child: ValueListenableBuilder<int>(\n                        valueListenable: TimedShutdownService().remainingSecondsNotifier,\n                        builder: (context, remainingSeconds, child) {\n                          return Text(\n                            remainingSeconds > 0\n                                ? \"定时关闭 (${TimedShutdownService().formatRemainingTime()})\"\n                                : \"定时关闭\",\n                          );\n                        },\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/plugin_editor/plugin_editor_page.dart",
    "content": "import 'package:card_settings_ui/card_settings_ui.dart';\nimport 'package:card_settings_ui/tile/settings_tile_info.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/plugins/anti_crawler_config.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\n\nclass PluginEditorPage extends StatefulWidget {\n  const PluginEditorPage({\n    super.key,\n  });\n\n  @override\n  State<PluginEditorPage> createState() => _PluginEditorPageState();\n}\n\nclass _PluginEditorPageState extends State<PluginEditorPage> {\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  final TextEditingController apiController = TextEditingController();\n  final TextEditingController typeController = TextEditingController();\n  final TextEditingController nameController = TextEditingController();\n  final TextEditingController versionController = TextEditingController();\n  final TextEditingController userAgentController = TextEditingController();\n  final TextEditingController baseURLController = TextEditingController();\n  final TextEditingController searchURLController = TextEditingController();\n  final TextEditingController searchListController = TextEditingController();\n  final TextEditingController searchNameController = TextEditingController();\n  final TextEditingController searchResultController = TextEditingController();\n  final TextEditingController chapterRoadsController = TextEditingController();\n  final TextEditingController chapterResultController = TextEditingController();\n  final TextEditingController refererController = TextEditingController();\n  bool muliSources = true;\n  bool useWebview = true;\n  bool useNativePlayer = true;\n  bool usePost = false;\n  bool useLegacyParser = false;\n  bool adBlocker = false;\n\n  // AntiCrawler fields\n  final TextEditingController captchaImageController = TextEditingController();\n  final TextEditingController captchaInputController = TextEditingController();\n  final TextEditingController captchaButtonController = TextEditingController();\n  bool antiCrawlerEnabled = false;\n  int captchaType = CaptchaType.imageCaptcha;\n  final MenuController captchaTypeMenuController = MenuController();\n\n  static const Map<int, String> _captchaTypeMap = {\n    CaptchaType.imageCaptcha: '图片验证码',\n    CaptchaType.autoClickButton: '自动点击按钮',\n  };\n\n  @override\n  void initState() {\n    super.initState();\n    final Plugin plugin = Modular.args.data as Plugin;\n    apiController.text = plugin.api;\n    typeController.text = plugin.type;\n    nameController.text = plugin.name;\n    versionController.text = plugin.version;\n    userAgentController.text = plugin.userAgent;\n    baseURLController.text = plugin.baseUrl;\n    searchURLController.text = plugin.searchURL;\n    searchListController.text = plugin.searchList;\n    searchNameController.text = plugin.searchName;\n    searchResultController.text = plugin.searchResult;\n    chapterRoadsController.text = plugin.chapterRoads;\n    chapterResultController.text = plugin.chapterResult;\n    refererController.text = plugin.referer;\n    muliSources = plugin.muliSources;\n    useWebview = plugin.useWebview;\n    useNativePlayer = plugin.useNativePlayer;\n    usePost = plugin.usePost;\n    useLegacyParser = plugin.useLegacyParser;\n    adBlocker = plugin.adBlocker;\n    antiCrawlerEnabled = plugin.antiCrawlerConfig.enabled;\n    captchaType = plugin.antiCrawlerConfig.captchaType;\n    captchaImageController.text = plugin.antiCrawlerConfig.captchaImage;\n    captchaInputController.text = plugin.antiCrawlerConfig.captchaInput;\n    captchaButtonController.text = plugin.antiCrawlerConfig.captchaButton;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Plugin plugin = Modular.args.data as Plugin;\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('规则编辑器'),\n      ),\n      body: SingleChildScrollView(\n        padding: const EdgeInsets.all(16.0),\n        child: Center(\n          child: SizedBox(\n            width: (MediaQuery.of(context).size.width > 1000) ? 1000 : null,\n            child: Column(\n              children: [\n                TextField(\n                  controller: nameController,\n                  decoration: const InputDecoration(\n                      labelText: 'Name', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: versionController,\n                  decoration: const InputDecoration(\n                      labelText: 'Version', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: baseURLController,\n                  decoration: const InputDecoration(\n                      labelText: 'BaseURL', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: searchURLController,\n                  decoration: const InputDecoration(\n                      labelText: 'SearchURL', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: searchListController,\n                  decoration: const InputDecoration(\n                      labelText: 'SearchList', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: searchNameController,\n                  decoration: const InputDecoration(\n                      labelText: 'SearchName', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: searchResultController,\n                  decoration: const InputDecoration(\n                      labelText: 'SearchResult', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: chapterRoadsController,\n                  decoration: const InputDecoration(\n                      labelText: 'ChapterRoads', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: chapterResultController,\n                  decoration: const InputDecoration(\n                      labelText: 'ChapterResult', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                ExpansionTile(\n                  title: const Text('高级选项'),\n                  shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),\n                  children: [\n                    SettingsSection(\n                      title: Text('行为设置', style: TextStyle(fontFamily: fontFamily)),\n                      tiles: [\n                        SettingsTile.switchTile(\n                          title: Text('简易解析', style: TextStyle(fontFamily: fontFamily)),\n                          description: Text('使用简易解析器而不是现代解析器', style: TextStyle(fontFamily: fontFamily)),\n                          initialValue: useLegacyParser,\n                          onToggle: (v) => setState(() => useLegacyParser = v ?? !useLegacyParser),\n                        ),\n                        SettingsTile.switchTile(\n                          title: Text('POST', style: TextStyle(fontFamily: fontFamily)),\n                          description: Text('使用 POST 而不是 GET 进行检索', style: TextStyle(fontFamily: fontFamily)),\n                          initialValue: usePost,\n                          onToggle: (v) => setState(() => usePost = v ?? !usePost),\n                        ),\n                        SettingsTile.switchTile(\n                          title: Text('内置播放器', style: TextStyle(fontFamily: fontFamily)),\n                          description: Text('使用内置播放器播放视频', style: TextStyle(fontFamily: fontFamily)),\n                          initialValue: useNativePlayer,\n                          onToggle: (v) => setState(() => useNativePlayer = v ?? !useNativePlayer),\n                        ),\n                        SettingsTile.switchTile(\n                          title: Text('广告过滤', style: TextStyle(fontFamily: fontFamily)),\n                          description: Text('启用 HLS 广告过滤', style: TextStyle(fontFamily: fontFamily)),\n                          initialValue: adBlocker,\n                          onToggle: (v) => setState(() => adBlocker = v ?? !adBlocker),\n                        ),\n                      ],\n                    ),\n                    SettingsSection(\n                      title: Text('网络设置', style: TextStyle(fontFamily: fontFamily)),\n                      tiles: [\n                        CustomSettingsTile(\n                          child: (info) => _buildTextFieldTile(\n                            context, info,\n                            controller: userAgentController,\n                            label: 'UserAgent',\n                          ),\n                        ),\n                        CustomSettingsTile(\n                          child: (info) => _buildTextFieldTile(\n                            context, info,\n                            controller: refererController,\n                            label: 'Referer',\n                          ),\n                        ),\n                      ],\n                    ),\n                    SettingsSection(\n                      title: Text('反反爬虫配置', style: TextStyle(fontFamily: fontFamily)),\n                      tiles: [\n                        SettingsTile.switchTile(\n                          title: Text('启用反反爬虫', style: TextStyle(fontFamily: fontFamily)),\n                          description: Text('检索失败时显示验证码验证按钮而非重试', style: TextStyle(fontFamily: fontFamily)),\n                          initialValue: antiCrawlerEnabled,\n                          onToggle: (v) => setState(() => antiCrawlerEnabled = v ?? !antiCrawlerEnabled),\n                        ),\n                        if (antiCrawlerEnabled) ...[\n                          SettingsTile.navigation(\n                            onPressed: (_) {\n                              if (captchaTypeMenuController.isOpen) {\n                                captchaTypeMenuController.close();\n                              } else {\n                                captchaTypeMenuController.open();\n                              }\n                            },\n                            title: Text('验证类型', style: TextStyle(fontFamily: fontFamily)),\n                            description: Text(\n                              captchaType == CaptchaType.imageCaptcha\n                                  ? '图片验证码（展示验证码图片，用户手动输入）'\n                                  : '自动点击验证按钮（检测到按钮后自动模拟点击）',\n                              style: TextStyle(fontFamily: fontFamily),\n                            ),\n                            value: MenuAnchor(\n                              consumeOutsideTap: true,\n                              controller: captchaTypeMenuController,\n                              builder: (_, __, ___) => Text(\n                                _captchaTypeMap[captchaType] ?? '未知',\n                                style: TextStyle(fontFamily: fontFamily),\n                              ),\n                              menuChildren: [\n                                for (final entry in _captchaTypeMap.entries)\n                                  MenuItemButton(\n                                    requestFocusOnHover: false,\n                                    onPressed: () => setState(() => captchaType = entry.key),\n                                    child: Container(\n                                      height: 48,\n                                      constraints: const BoxConstraints(minWidth: 160),\n                                      child: Align(\n                                        alignment: Alignment.centerLeft,\n                                        child: Text(\n                                          entry.value,\n                                          style: TextStyle(\n                                            color: entry.key == captchaType\n                                                ? Theme.of(context).colorScheme.primary\n                                                : null,\n                                            fontFamily: fontFamily,\n                                          ),\n                                        ),\n                                      ),\n                                    ),\n                                  ),\n                              ],\n                            ),\n                          ),\n                          if (captchaType == CaptchaType.imageCaptcha) ...[\n                            CustomSettingsTile(\n                              child: (info) => _buildTextFieldTile(\n                                context, info,\n                                controller: captchaImageController,\n                                label: 'CaptchaImage (XPath)',\n                                hint: '//img[@class=\"captcha\"]',\n                                helper: '验证码图片元素的 XPath',\n                              ),\n                            ),\n                            CustomSettingsTile(\n                              child: (info) => _buildTextFieldTile(\n                                context, info,\n                                controller: captchaInputController,\n                                label: 'CaptchaInput (XPath)',\n                                hint: '//input[@name=\"captcha\"]',\n                                helper: '验证码输入框元素的 XPath',\n                              ),\n                            ),\n                          ],\n                          CustomSettingsTile(\n                            child: (info) => _buildTextFieldTile(\n                              context, info,\n                              controller: captchaButtonController,\n                              label: captchaType == CaptchaType.imageCaptcha\n                                  ? 'CaptchaButton (XPath)'\n                                  : 'VerifyButton (XPath)',\n                              hint: '//button[@type=\"submit\"]',\n                              helper: captchaType == CaptchaType.imageCaptcha\n                                  ? '验证提交按钮元素的 XPath'\n                                  : '验证按钮元素的 XPath，检测到后自动点击',\n                            ),\n                          ),\n                        ],\n                      ],\n                    ),\n                  ],\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n      floatingActionButton: Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        children: [\n          FloatingActionButton(\n            heroTag: null,\n            child: const Icon(Icons.bug_report),\n            onPressed: () async {\n              Plugin pluginText = Plugin(\n                  api: apiController.text,\n                  type: typeController.text,\n                  name: nameController.text,\n                  version: versionController.text,\n                  muliSources: muliSources,\n                  useWebview: useWebview,\n                  useNativePlayer: useNativePlayer,\n                  usePost: usePost,\n                  useLegacyParser: useLegacyParser,\n                  adBlocker: adBlocker,\n                  userAgent: userAgentController.text,\n                  baseUrl: baseURLController.text,\n                  searchURL: searchURLController.text,\n                  searchList: searchListController.text,\n                  searchName: searchNameController.text,\n                  searchResult: searchResultController.text,\n                  chapterRoads: chapterRoadsController.text,\n                  chapterResult: chapterResultController.text,\n                  referer: refererController.text,\n                  antiCrawlerConfig: AntiCrawlerConfig(\n                    enabled: antiCrawlerEnabled,\n                    captchaType: captchaType,\n                    captchaImage: captchaImageController.text,\n                    captchaInput: captchaInputController.text,\n                    captchaButton: captchaButtonController.text,\n                  ));\n              Modular.to.pushNamed('/settings/plugin/test', arguments: pluginText);\n            },\n          ),\n          SizedBox(width: 15),\n          FloatingActionButton(\n            heroTag: null,\n            child: const Icon(Icons.save),\n            onPressed: () async {\n              plugin.api = apiController.text;\n              plugin.type = typeController.text;\n              plugin.name = nameController.text;\n              plugin.version = versionController.text;\n              plugin.userAgent = userAgentController.text;\n              plugin.baseUrl = baseURLController.text;\n              plugin.searchURL = searchURLController.text;\n              plugin.searchList = searchListController.text;\n              plugin.searchName = searchNameController.text;\n              plugin.searchResult = searchResultController.text;\n              plugin.chapterRoads = chapterRoadsController.text;\n              plugin.chapterResult = chapterResultController.text;\n              plugin.muliSources = muliSources;\n              plugin.useWebview = useWebview;\n              plugin.useNativePlayer = useNativePlayer;\n              plugin.usePost = usePost;\n              plugin.useLegacyParser = useLegacyParser;\n              plugin.adBlocker = adBlocker;\n              plugin.referer = refererController.text;\n              plugin.antiCrawlerConfig = AntiCrawlerConfig(\n                enabled: antiCrawlerEnabled,\n                captchaType: captchaType,\n                captchaImage: captchaImageController.text,\n                captchaInput: captchaInputController.text,\n                captchaButton: captchaButtonController.text,\n              );\n              pluginsController.updatePlugin(plugin);\n              Navigator.of(context).pop();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTextFieldTile(\n    BuildContext context,\n    SettingsTileInfo info, {\n    required TextEditingController controller,\n    required String label,\n    String? hint,\n    String? helper,\n  }) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        ClipRRect(\n          borderRadius: BorderRadius.vertical(\n            top: Radius.circular(info.isTopTile ? 20 : 3),\n            bottom: Radius.circular(info.isBottomTile ? 20 : 3),\n          ),\n          child: Material(\n            color: Theme.of(context).brightness == Brightness.light\n                ? Theme.of(context).colorScheme.surfaceContainerLowest\n                : Theme.of(context).colorScheme.surfaceContainerHigh,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\n              child: TextField(\n                controller: controller,\n                decoration: InputDecoration(\n                  labelText: label,\n                  hintText: hint,\n                  helperText: helper,\n                  border: const OutlineInputBorder(),\n                ),\n              ),\n            ),\n          ),\n        ),\n        if (info.needDivider) const SizedBox(height: 2),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/plugin_editor/plugin_module.dart",
    "content": "import 'package:kazumi/pages/plugin_editor/plugin_test_page.dart';\nimport 'package:kazumi/pages/plugin_editor/plugin_view_page.dart';\nimport 'package:kazumi/pages/plugin_editor/plugin_editor_page.dart';\nimport 'package:kazumi/pages/plugin_editor/plugin_shop_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass PluginModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const PluginViewPage());\n    r.child(\"/shop\", child: (_) => const PluginShopPage());\n    r.child(\"/test\",\n        child: (_) => const PluginTestPage(),\n        transition: TransitionType.defaultTransition);\n    r.child(\"/editor\",\n        child: (_) => const PluginEditorPage(),\n        transition: TransitionType.defaultTransition);\n  }\n}\n"
  },
  {
    "path": "lib/pages/plugin_editor/plugin_shop_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\n\nclass PluginShopPage extends StatefulWidget {\n  const PluginShopPage({super.key});\n\n  @override\n  State<PluginShopPage> createState() => _PluginShopPageState();\n}\n\nclass _PluginShopPageState extends State<PluginShopPage> {\n  Box setting = GStorage.setting;\n  bool timeout = false;\n  bool loading = false;\n  late bool enableGitProxy;\n\n  // 排序方式状态：false=按更新时间排序，true=按名称排序\n  bool sortByName = false;\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    enableGitProxy =\n        setting.get(SettingBoxKey.enableGitProxy, defaultValue: false);\n  }\n\n  // 刷新规则列表\n  void _handleRefresh() async {\n    if (!loading) {\n      setState(() {\n        loading = true;\n        timeout = false;\n      });\n      enableGitProxy =\n          setting.get(SettingBoxKey.enableGitProxy, defaultValue: false);\n      pluginsController.queryPluginHTTPList().then((_) {\n        setState(() {\n          loading = false;\n        });\n        if (pluginsController.pluginHTTPList.isEmpty) {\n          setState(() {\n            timeout = true;\n          });\n        }\n      });\n    }\n  }\n\n  // 切换排序方式\n  void _toggleSort() {\n    setState(() {\n      sortByName = !sortByName;\n    });\n  }\n\n  Widget get pluginHTTPListBody {\n    return Observer(builder: (context) {\n      // 创建列表副本用于排序\n      var sortedList = List.from(pluginsController.pluginHTTPList);\n\n      // 排序规则：\n      // 1. 按名称排序：忽略大小写的字母顺序\n      // 2. 按时间排序：更新时间降序（最新的在前面）\n      if (sortByName) {\n        sortedList.sort(\n            (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));\n      } else {\n        sortedList.sort((a, b) => b.lastUpdate.compareTo(a.lastUpdate));\n      }\n\n      return ListView.builder(\n        itemCount: sortedList.length,\n        itemBuilder: (context, index) {\n          return Card(\n            margin: const EdgeInsets.fromLTRB(8, 0, 8, 8),\n            child: ListTile(\n                title: Row(\n                  children: [\n                    Text(\n                      sortedList[index].name,\n                      style: const TextStyle(fontWeight: FontWeight.bold),\n                    ),\n                  ],\n                ),\n                subtitle: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Row(\n                      children: [\n                        Container(\n                          padding: const EdgeInsets.symmetric(\n                              horizontal: 8.0, vertical: 1.0),\n                          decoration: BoxDecoration(\n                            color: Theme.of(context).colorScheme.secondary,\n                            borderRadius: BorderRadius.circular(16.0),\n                          ),\n                          child: Text(\n                            sortedList[index].version,\n                            style: TextStyle(\n                                color: Theme.of(context).colorScheme.surface),\n                          ),\n                        ),\n                        const SizedBox(width: 5),\n                        Container(\n                          padding: const EdgeInsets.symmetric(\n                              horizontal: 8.0, vertical: 1.0),\n                          decoration: BoxDecoration(\n                            color: Theme.of(context).colorScheme.primary,\n                            borderRadius: BorderRadius.circular(16.0),\n                          ),\n                          child: Text(\n                            sortedList[index].useNativePlayer\n                                ? \"native\"\n                                : \"webview\",\n                            style: TextStyle(\n                                color: Theme.of(context).colorScheme.surface),\n                          ),\n                        ),\n                        if (sortedList[index].antiCrawlerEnabled) ...[  \n                          const SizedBox(width: 5),\n                          Container(\n                            padding: const EdgeInsets.symmetric(\n                                horizontal: 8.0, vertical: 1.0),\n                            decoration: BoxDecoration(\n                              color: Theme.of(context).colorScheme.tertiary,\n                              borderRadius: BorderRadius.circular(16.0),\n                            ),\n                            child: Text(\n                              'captcha',\n                              style: TextStyle(\n                                  color: Theme.of(context)\n                                      .colorScheme\n                                      .onTertiary),\n                            ),\n                          ),\n                        ],\n                      ],\n                    ),\n                    if (sortedList[index].lastUpdate > 0) ...[\n                      const SizedBox(height: 4),\n                      Text(\n                        '更新时间: ${DateTime.fromMillisecondsSinceEpoch(sortedList[index].lastUpdate).toString().split('.')[0]}',\n                        style: const TextStyle(color: Colors.grey),\n                      ),\n                    ],\n                  ],\n                ),\n                trailing: TextButton(\n                  onPressed: () async {\n                    if (pluginsController.pluginStatus(sortedList[index]) ==\n                        'install') {\n                      KazumiDialog.showToast(message: '导入中');\n                      int res = await pluginsController\n                          .tryUpdatePluginByName(sortedList[index].name);\n                      if (res == 0) {\n                        KazumiDialog.showToast(message: '导入成功');\n                        setState(() {});\n                      } else if (res == 1) {\n                        KazumiDialog.showToast(\n                            message: 'kazumi版本过低, 此规则不兼容当前版本');\n                      } else if (res == 2) {\n                        KazumiDialog.showToast(message: '导入规则失败');\n                      }\n                    }\n                    if (pluginsController.pluginStatus(sortedList[index]) ==\n                        'update') {\n                      KazumiDialog.showToast(message: '更新中');\n                      int res = await pluginsController\n                          .tryUpdatePluginByName(sortedList[index].name);\n                      if (res == 0) {\n                        KazumiDialog.showToast(message: '更新成功');\n                        setState(() {});\n                      } else if (res == 1) {\n                        KazumiDialog.showToast(\n                            message: 'kazumi版本过低, 此规则不兼容当前版本');\n                      } else if (res == 2) {\n                        KazumiDialog.showToast(message: '更新规则失败');\n                      }\n                    }\n                  },\n                  child: Text(pluginsController\n                              .pluginStatus(sortedList[index]) ==\n                          'install'\n                      ? '安装'\n                      : (pluginsController.pluginStatus(sortedList[index]) ==\n                              'installed')\n                          ? '已安装'\n                          : '更新'),\n                )),\n          );\n        },\n      );\n    });\n  }\n\n  Widget get timeoutWidget {\n    return Center(\n      child: GeneralErrorWidget(\n        errMsg: '啊咧（⊙.⊙） 无法访问远程仓库\\n${enableGitProxy ? '镜像已启用' : '镜像已禁用'}',\n        actions: [\n          GeneralErrorButton(\n            onPressed: () {\n              Modular.to.pushNamed('/settings/webdav/');\n            },\n            text: enableGitProxy ? '禁用镜像' : '启用镜像',\n          ),\n          GeneralErrorButton(\n            onPressed: () {\n              _handleRefresh();\n            },\n            text: '刷新',\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {});\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: SysAppBar(\n          title: const Text('规则仓库'),\n          actions: [\n            IconButton(\n                onPressed: _toggleSort,\n                tooltip: sortByName ? '按名称排序' : '按更新时间排序',\n                icon:\n                    Icon(sortByName ? Icons.sort_by_alpha : Icons.access_time)),\n            IconButton(\n                onPressed: () {\n                  _handleRefresh();\n                },\n                tooltip: '刷新规则列表',\n                icon: const Icon(Icons.refresh))\n          ],\n        ),\n        body: loading\n            ? (const Center(child: CircularProgressIndicator()))\n            : (pluginsController.pluginHTTPList.isEmpty\n                ? timeoutWidget\n                : pluginHTTPListBody),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/plugin_editor/plugin_test_page.dart",
    "content": "import 'package:dio/dio.dart';\nimport 'package:flutter/material.dart' hide Element;\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/modules/search/plugin_search_module.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:html/dom.dart' show Element;\nimport 'package:html/parser.dart' show parse;\nimport 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart';\n\nimport '../../modules/roads/road_module.dart';\nimport '../../plugins/plugins.dart';\n\nconst _h8 = SizedBox(height: 8.0);\nconst _h12 = SizedBox(height: 12.0);\n\n// 简化配色映射：仅三类核心色\nenum CoreColorType { error, success, waiting }\n\nextension CoreColorExtension on ThemeData {\n  Color getCoreColor(CoreColorType type) {\n    switch (type) {\n      case CoreColorType.error:\n        return colorScheme.error;\n      case CoreColorType.success:\n        return colorScheme.primary;\n      case CoreColorType.waiting:\n        return colorScheme.onSurfaceVariant;\n    }\n  }\n}\n\nclass PluginTestPage extends StatefulWidget {\n  const PluginTestPage({super.key});\n\n  @override\n  State<PluginTestPage> createState() => _PluginTestPageState();\n}\n\nclass _PluginTestPageState extends State<PluginTestPage> {\n  late final Plugin plugin;\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final testKeywordController = TextEditingController();\n  final htmlScrollController = ScrollController();\n  final chapterScrollController = ScrollController();\n  final itemHtmlScrollController = ScrollController();\n\n  String searchHtml = \"\";\n  PluginSearchResponse? searchRes;\n  List<Road>? chapters;\n  bool isTesting = false;\n  String errorMsg = \"\";\n  final Map<int, String> _itemHtmlMap = {};\n  int? _showItemHtmlIdx;\n\n  bool get _hasSearchHtml => searchHtml.isNotEmpty;\n\n  bool get _hasSearchData => searchRes?.data.isNotEmpty ?? false;\n\n  bool get _hasChapters => chapters?.isNotEmpty ?? false;\n\n  bool get _needChapterParse => plugin.chapterRoads.isNotEmpty;\n\n  CancelToken? _testSearchRequestCancelToken;\n  CancelToken? _testRoadsCancelToken;\n\n  @override\n  void initState() {\n    super.initState();\n    plugin = Modular.args.data as Plugin;\n    testKeywordController.addListener(\n        () => errorMsg.isNotEmpty ? setState(() => errorMsg = \"\") : null);\n  }\n\n  @override\n  void dispose() {\n    _testSearchRequestCancelToken?.cancel();\n    _testRoadsCancelToken?.cancel();\n    testKeywordController.dispose();\n    htmlScrollController.dispose();\n    chapterScrollController.dispose();\n    itemHtmlScrollController.dispose();\n    super.dispose();\n  }\n\n  void onBackPressed() =>\n      KazumiDialog.observer.hasKazumiDialog ? KazumiDialog.dismiss() : null;\n\n  void resetState() => setState(() {\n        _testSearchRequestCancelToken?.cancel();\n        _testSearchRequestCancelToken = null;\n        _testRoadsCancelToken?.cancel();\n        _testRoadsCancelToken = null;\n        searchHtml = \"\";\n        searchRes = null;\n        chapters = null;\n        errorMsg = \"\";\n        _itemHtmlMap.clear();\n        _showItemHtmlIdx = null;\n      });\n\n  String _parseItemHtml(int index) {\n    if (_itemHtmlMap.containsKey(index)) return _itemHtmlMap[index]!;\n    try {\n      final node = (parse(searchHtml)\n          .documentElement!\n          .queryXPath(plugin.searchList)\n          .nodes[index]\n          .node as Element);\n      return _itemHtmlMap[index] = node.outerHtml;\n    } catch (e) {\n      KazumiLogger().e('PluginTest: failed to parse HTML item ${index + 1}', error: e);\n      return \"解析失败：$e\";\n    }\n  }\n\n  void _toggleItemHtml(int index) {\n    if (_showItemHtmlIdx == index)\n      return setState(() => _showItemHtmlIdx = null);\n    setState(() => isTesting = true);\n    _parseItemHtml(index);\n    setState(() {\n      _showItemHtmlIdx = index;\n      isTesting = false;\n    });\n  }\n\n  Future<void> startTest() async {\n    final keyword = testKeywordController.text.trim();\n    resetState();\n    setState(() => isTesting = true);\n    try {\n      _testSearchRequestCancelToken?.cancel();\n      _testSearchRequestCancelToken = CancelToken();\n      searchHtml = await plugin.testSearchRequest(keyword,\n          shouldRethrow: true, cancelToken: _testSearchRequestCancelToken);\n      searchRes = plugin.testQueryBangumi(searchHtml);\n      if (_hasSearchData && _needChapterParse) {\n        final firstItem = searchRes!.data.first;\n        if (firstItem.src.isNotEmpty) {\n          _testRoadsCancelToken?.cancel();\n          _testRoadsCancelToken = CancelToken();\n          chapters = await plugin.querychapterRoads(firstItem.src,\n              cancelToken: _testRoadsCancelToken);\n        }\n      }\n    } catch (e, stack) {\n      KazumiLogger().e(\"PluginTest: test failed\", error: e, stackTrace: stack);\n    } finally {\n      if (mounted) setState(() => isTesting = false);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (didPop, _) => !didPop ? onBackPressed() : null,\n      child: Scaffold(\n        appBar: SysAppBar(\n          title: Text('${plugin.name} 测试'),\n          actions: [\n            IconButton(\n              onPressed: isTesting ? null : startTest,\n              icon: const Icon(Icons.bug_report_outlined),\n              tooltip: '开始测试',\n            ),\n            IconButton(\n              onPressed: resetState,\n              icon: const Icon(Icons.refresh),\n              tooltip: '重置',\n            ),\n          ],\n        ),\n        body: SingleChildScrollView(\n          padding: const EdgeInsets.all(16.0),\n          child: Center(\n            child: ConstrainedBox(\n              constraints: const BoxConstraints(maxWidth: 1000),\n              child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    _buildKeywordInput(theme),\n                    _h12,\n                    _buildErrorWidget(theme),\n                    _buildExpansionTile(\n                      theme: theme,\n                      title: '1. 搜索请求测试',\n                      subtitle: _getSearchSubtitle(),\n                      expanded: false,\n                      child: _buildSearchContent(theme),\n                    ),\n                    _h12,\n                    _buildExpansionTile(\n                      theme: theme,\n                      title: '2. 搜索解析测试',\n                      subtitle: _getParseSubtitle(),\n                      expanded: false,\n                      child: _buildParseContent(theme),\n                    ),\n                    _h12,\n                    _buildExpansionTile(\n                      theme: theme,\n                      title: '3. 章节列表测试',\n                      subtitle: _getChapterSubtitle(),\n                      expanded: _hasSearchData,\n                      child: _buildChapterContent(theme),\n                    ),\n                  ]),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildExpansionTile({\n    required ThemeData theme,\n    required String title,\n    required String subtitle,\n    required bool expanded,\n    required Widget child,\n  }) {\n    return ExpansionTile(\n      title: Text(title, style: theme.textTheme.titleMedium),\n      subtitle: Text(subtitle,\n          style: TextStyle(\n              fontSize: 12.0, color: _getSubtitleColor(subtitle, theme))),\n      initiallyExpanded: expanded,\n      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),\n      iconColor: theme.getCoreColor(CoreColorType.success),\n      collapsedIconColor: theme.getCoreColor(CoreColorType.waiting),\n      children: [_h8, child, _h8],\n    );\n  }\n\n  Widget _buildKeywordInput(ThemeData theme) => TextField(\n        controller: testKeywordController,\n        decoration: InputDecoration(\n          labelText: '测试关键词',\n          border: OutlineInputBorder(\n              borderSide:\n                  BorderSide(color: theme.getCoreColor(CoreColorType.waiting))),\n          focusedBorder: OutlineInputBorder(\n              borderSide:\n                  BorderSide(color: theme.getCoreColor(CoreColorType.success))),\n          labelStyle:\n              TextStyle(color: theme.getCoreColor(CoreColorType.waiting)),\n        ),\n        enabled: !isTesting,\n        onSubmitted: (_) => startTest(),\n        style: theme.textTheme.bodyLarge,\n      );\n\n  Widget _buildErrorWidget(ThemeData theme) => errorMsg.isEmpty || isTesting\n      ? const SizedBox.shrink()\n      : Container(\n          padding: const EdgeInsets.all(12.0),\n          decoration: BoxDecoration(\n            color: theme.colorScheme.errorContainer,\n            border: Border.all(color: theme.getCoreColor(CoreColorType.error)),\n            borderRadius: BorderRadius.circular(8),\n          ),\n          child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [\n            Icon(Icons.error_outline,\n                color: theme.getCoreColor(CoreColorType.error), size: 20),\n            _h8,\n            Expanded(\n              child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(errorMsg,\n                        style: theme.textTheme.bodyMedium?.copyWith(\n                            color: theme.colorScheme.onErrorContainer)),\n                    _h8,\n                    TextButton(\n                      onPressed: startTest,\n                      style: TextButton.styleFrom(\n                          backgroundColor: theme\n                              .getCoreColor(CoreColorType.error)\n                              .withOpacity(0.1)),\n                      child: Text('重试测试',\n                          style: TextStyle(\n                              color: theme.colorScheme.onErrorContainer)),\n                    ),\n                  ]),\n            ),\n          ]),\n        );\n\n  Widget _buildLoading(ThemeData theme) => Center(\n        child: CircularProgressIndicator.adaptive(\n          valueColor: AlwaysStoppedAnimation<Color>(\n              theme.getCoreColor(CoreColorType.success)),\n        ),\n      );\n\n  Widget _buildEmpty(String text, ThemeData theme, {bool isError = false}) =>\n      Center(\n        child: Padding(\n          padding: const EdgeInsets.symmetric(vertical: 16),\n          child: Text(\n            text,\n            style: theme.textTheme.bodyMedium?.copyWith(\n              color: isError\n                  ? theme.getCoreColor(CoreColorType.error)\n                  : theme.getCoreColor(CoreColorType.waiting),\n            ),\n          ),\n        ),\n      );\n\n  String _getSearchSubtitle() {\n    if (isTesting) return '测试中...';\n    if (!_hasSearchHtml) return '未执行测试';\n    return 'HTML长度：${searchHtml.length} 字符';\n  }\n\n  // 简化副标题颜色逻辑：仅三类\n  Color _getSubtitleColor(String subtitle, ThemeData theme) {\n    if (subtitle.contains('测试中') ||\n        subtitle.contains('获取中') ||\n        subtitle.contains('解析中')) {\n      return theme.getCoreColor(CoreColorType.waiting);\n    }\n    if (subtitle.contains('失败') ||\n        subtitle.contains('无可用') ||\n        subtitle.contains('无有效')) {\n      return theme.getCoreColor(CoreColorType.error);\n    }\n    return theme.getCoreColor(CoreColorType.success);\n  }\n\n  Widget _buildSearchContent(ThemeData theme) {\n    if (isTesting) return _buildLoading(theme);\n    if (!_hasSearchHtml) return _buildEmpty('点击顶部「开始测试」按钮执行', theme);\n    return Container(\n      margin: const EdgeInsets.only(bottom: 8.0),\n      padding: const EdgeInsets.all(8.0),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: theme.getCoreColor(CoreColorType.waiting)),\n        color: theme.colorScheme.surface,\n      ),\n      height: 250,\n      child: SingleChildScrollView(\n        controller: htmlScrollController,\n        physics: const ClampingScrollPhysics(),\n        child: SelectableText(\n          searchHtml,\n          style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),\n        ),\n      ),\n    );\n  }\n\n  String _getParseSubtitle() {\n    if (isTesting && _showItemHtmlIdx == null) return '解析中...';\n    if (!_hasSearchHtml) return '未执行解析';\n    if (!_hasSearchData) return '未解析到结果';\n    return '解析到 ${searchRes?.data.length ?? 0} 条结果';\n  }\n\n  Widget _buildParseContent(ThemeData theme) {\n    if (isTesting && _showItemHtmlIdx == null) return _buildLoading(theme);\n    if (!_hasSearchHtml) return _buildEmpty('请先完成搜索请求测试', theme);\n    if (!_hasSearchData) return _buildEmpty('未解析到搜索结果', theme, isError: true);\n\n    return Column(children: [\n      ListView.builder(\n        shrinkWrap: true,\n        physics: const NeverScrollableScrollPhysics(),\n        itemCount: searchRes!.data.length,\n        itemBuilder: (_, i) =>\n            _buildSearchItemCard(searchRes!.data[i], i, theme),\n      ),\n      _h8,\n    ]);\n  }\n\n  Widget _buildSearchItemCard(SearchItem item, int i, ThemeData theme) {\n    final isShowHtml = _showItemHtmlIdx == i;\n    final itemHtml = _itemHtmlMap[i] ?? '加载中...';\n\n    return Column(children: [\n      Card(\n        margin: const EdgeInsets.only(bottom: 8.0),\n        elevation: 1,\n        child: Padding(\n          padding: const EdgeInsets.all(12.0),\n          child:\n              Column(crossAxisAlignment: CrossAxisAlignment.start, children: [\n            Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [\n              Expanded(\n                child: Text(\n                  '${i + 1}：${item.name}',\n                  style: theme.textTheme.titleMedium\n                      ?.copyWith(fontWeight: FontWeight.w500),\n                  maxLines: 2,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              IconButton(\n                onPressed: isTesting ? null : () => _toggleItemHtml(i),\n                icon: Icon(\n                  isShowHtml ? Icons.keyboard_arrow_up : Icons.code,\n                  size: 18,\n                  color: theme.getCoreColor(CoreColorType.success),\n                ),\n                tooltip: isShowHtml ? '隐藏HTML' : '查看HTML',\n              ),\n            ]),\n            _h8,\n            Text('链接：${item.src}',\n                style: theme.textTheme.bodySmall?.copyWith(\n                    color: theme.getCoreColor(CoreColorType.waiting))),\n          ]),\n        ),\n      ),\n      if (isShowHtml)\n        Container(\n          margin: const EdgeInsets.only(bottom: 8.0),\n          padding: const EdgeInsets.all(8.0),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(8),\n            border:\n                Border.all(color: theme.getCoreColor(CoreColorType.waiting)),\n            color: theme.colorScheme.surface,\n          ),\n          height: 250,\n          child: SingleChildScrollView(\n            controller: itemHtmlScrollController,\n            physics: const ClampingScrollPhysics(),\n            child: SelectableText(\n              itemHtml,\n              style:\n                  theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),\n            ),\n          ),\n        ),\n    ]);\n  }\n\n  String _getChapterSubtitle() {\n    if (isTesting) return '获取中...';\n    if (!_hasSearchData) return '无有效搜索结果';\n    if (!_needChapterParse) return '无需解析章节';\n    if (chapters == null) return '未获取章节数据';\n    return '获取到 ${chapters?.length ?? 0} 个播放列表';\n  }\n\n  Widget _buildChapterContent(ThemeData theme) {\n    if (!_needChapterParse) return _buildEmpty('未填写章节规则', theme);\n    if (isTesting) return _buildLoading(theme);\n    if (!_hasSearchData) return _buildEmpty('请先解析到有效结果', theme);\n    if (chapters == null) return _buildEmpty('未获取章节数据', theme, isError: true);\n    if (!_hasChapters) return _buildEmpty('无可用章节', theme, isError: true);\n\n    return Container(\n      padding: const EdgeInsets.all(8.0),\n      height: 280,\n      child: ListView.builder(\n        controller: chapterScrollController,\n        itemCount: chapters?.length ?? 0,\n        itemBuilder: (_, i) => _buildChapterCard(chapters![i], i, theme),\n      ),\n    );\n  }\n\n  Widget _buildChapterCard(Road road, int i, ThemeData theme) => Card(\n        margin: const EdgeInsets.only(bottom: 8.0),\n        elevation: 1,\n        child: Padding(\n          padding: const EdgeInsets.all(12.0),\n          child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                Text(\n                  '播放列表 ${i + 1}：${road.name}',\n                  style: theme.textTheme.titleMedium\n                      ?.copyWith(fontWeight: FontWeight.w500),\n                ),\n                _h8,\n                Text('章节数量：${road.data.length}',\n                    style: theme.textTheme.bodySmall?.copyWith(\n                        color: theme.getCoreColor(CoreColorType.waiting))),\n                _h8,\n                SizedBox(\n                  width: double.infinity,\n                  height: 120,\n                  child: SingleChildScrollView(\n                    child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          ...road.identifier.asMap().entries.map((e) => Text(\n                                '${e.key + 1}. ${e.value}',\n                                style: theme.textTheme.bodySmall,\n                              )),\n                        ]),\n                  ),\n                ),\n              ]),\n        ),\n      );\n}\n"
  },
  {
    "path": "lib/pages/plugin_editor/plugin_view_page.dart",
    "content": "import 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\n\nclass PluginViewPage extends StatefulWidget {\n  const PluginViewPage({super.key});\n\n  @override\n  State<PluginViewPage> createState() => _PluginViewPageState();\n}\n\nclass _PluginViewPageState extends State<PluginViewPage> {\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n\n  // 是否处于多选模式\n  bool isMultiSelectMode = false;\n\n  // 已选中的规则名称集合\n  final Set<String> selectedNames = {};\n\n  Future<void> _handleUpdate() async {\n    KazumiDialog.showLoading(msg: '更新中');\n    int count = await pluginsController.tryUpdateAllPlugin();\n    KazumiDialog.dismiss();\n    if (count == 0) {\n      KazumiDialog.showToast(message: '所有规则已是最新');\n    } else {\n      KazumiDialog.showToast(message: '更新成功 $count 条');\n    }\n  }\n\n  void _handleAdd() {\n    KazumiDialog.show(builder: (context) {\n      return AlertDialog(\n        // contentPadding: EdgeInsets.zero, // 设置为零以减小内边距\n        content: SingleChildScrollView(\n          // 使用可滚动的SingleChildScrollView包装Column\n          child: Column(\n            mainAxisSize: MainAxisSize.min, // 设置为MainAxisSize.min以减小高度\n            children: [\n              ListTile(\n                title: const Text('新建规则'),\n                onTap: () {\n                  KazumiDialog.dismiss();\n                  Modular.to.pushNamed('/settings/plugin/editor',\n                      arguments: Plugin.fromTemplate());\n                },\n              ),\n              const SizedBox(height: 10),\n              ListTile(\n                title: const Text('从规则仓库导入'),\n                onTap: () {\n                  KazumiDialog.dismiss();\n                  Modular.to.pushNamed('/settings/plugin/shop',\n                      arguments: Plugin.fromTemplate());\n                },\n              ),\n              const SizedBox(height: 10),\n              ListTile(\n                title: const Text('从剪贴板导入'),\n                onTap: () {\n                  KazumiDialog.dismiss();\n                  _showInputDialog();\n                },\n              ),\n            ],\n          ),\n        ),\n      );\n    });\n  }\n\n  void _showInputDialog() {\n    final TextEditingController textController = TextEditingController();\n    KazumiDialog.show(builder: (context) {\n      return AlertDialog(\n        title: const Text('导入规则'),\n        content: StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return TextField(\n            controller: textController,\n          );\n        }),\n        actions: [\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          StatefulBuilder(\n              builder: (BuildContext context, StateSetter setState) {\n            return TextButton(\n              onPressed: () async {\n                final String msg = textController.text;\n                try {\n                  pluginsController.updatePlugin(Plugin.fromJson(\n                      json.decode(Utils.kazumiBase64ToJson(msg))));\n                  KazumiDialog.showToast(message: '导入成功');\n                } catch (e) {\n                  KazumiDialog.dismiss();\n                  KazumiDialog.showToast(message: '导入失败 ${e.toString()}');\n                }\n                KazumiDialog.dismiss();\n              },\n              child: const Text('导入'),\n            );\n          })\n        ],\n      );\n    });\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {});\n    return PopScope(\n      canPop: !isMultiSelectMode,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (isMultiSelectMode) {\n          setState(() {\n            isMultiSelectMode = false;\n            selectedNames.clear();\n          });\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: SysAppBar(\n          title: isMultiSelectMode\n              ? Text('已选择 ${selectedNames.length} 项')\n              : const Text('规则管理'),\n          leading: isMultiSelectMode\n              ? IconButton(\n                  icon: const Icon(Icons.close),\n                  onPressed: () {\n                    setState(() {\n                      isMultiSelectMode = false;\n                      selectedNames.clear();\n                    });\n                  },\n                )\n              : null,\n          actions: [\n            if (isMultiSelectMode) ...[\n              IconButton(\n                onPressed: selectedNames.isEmpty\n                    ? null\n                    : () {\n                        KazumiDialog.show(\n                          builder: (context) => AlertDialog(\n                            title: const Text('删除规则'),\n                            content:\n                                Text('确定要删除选中的 ${selectedNames.length} 条规则吗？'),\n                            actions: [\n                              TextButton(\n                                onPressed: () => KazumiDialog.dismiss(),\n                                child: Text(\n                                  '取消',\n                                  style: TextStyle(\n                                      color: Theme.of(context)\n                                          .colorScheme\n                                          .outline),\n                                ),\n                              ),\n                              TextButton(\n                                onPressed: () {\n                                  pluginsController\n                                      .removePlugins(selectedNames);\n                                  setState(() {\n                                    isMultiSelectMode = false;\n                                    selectedNames.clear();\n                                  });\n                                  KazumiDialog.dismiss();\n                                },\n                                child: const Text('删除'),\n                              ),\n                            ],\n                          ),\n                        );\n                      },\n                icon: const Icon(Icons.delete),\n              ),\n            ] else ...[\n              IconButton(\n                onPressed: () {\n                  _handleUpdate();\n                },\n                tooltip: '更新全部',\n                icon: const Icon(Icons.update),\n              ),\n              IconButton(\n                onPressed: () {\n                  _handleAdd();\n                },\n                tooltip: '添加规则',\n                icon: const Icon(Icons.add),\n              )\n            ],\n          ],\n        ),\n        body: Observer(builder: (context) {\n          return pluginsController.pluginList.isEmpty\n              ? const Center(\n                  child: Text('啊咧（⊙.⊙） 没有可用规则的说'),\n                )\n              : Builder(builder: (context) {\n                  return ReorderableListView.builder(\n                      buildDefaultDragHandles: false,\n                      proxyDecorator: (child, index, animation) {\n                        return Material(\n                          elevation: 0,\n                          color: Colors.transparent,\n                          child: child,\n                        );\n                      },\n                      onReorder: (int oldIndex, int newIndex) {\n                        pluginsController.onReorder(oldIndex, newIndex);\n                      },\n                      itemCount: pluginsController.pluginList.length,\n                      itemBuilder: (context, index) {\n                        var plugin = pluginsController.pluginList[index];\n                        bool canUpdate =\n                            pluginsController.pluginUpdateStatus(plugin) ==\n                                'updatable';\n                        return Card(\n                            key: ValueKey(index),\n                            margin: const EdgeInsets.fromLTRB(8, 0, 8, 8),\n                            child: ListTile(\n                              trailing: pluginCardTrailing(index),\n                              shape: RoundedRectangleBorder(\n                                  borderRadius: BorderRadius.circular(12)),\n                              onLongPress: () {\n                                if (!isMultiSelectMode) {\n                                  setState(() {\n                                    isMultiSelectMode = true;\n                                    selectedNames.add(plugin.name);\n                                  });\n                                }\n                              },\n                              onTap: () {\n                                if (isMultiSelectMode) {\n                                  setState(() {\n                                    if (selectedNames.contains(plugin.name)) {\n                                      selectedNames.remove(plugin.name);\n                                      if (selectedNames.isEmpty) {\n                                        isMultiSelectMode = false;\n                                      }\n                                    } else {\n                                      selectedNames.add(plugin.name);\n                                    }\n                                  });\n                                }\n                              },\n                              selected: selectedNames.contains(plugin.name),\n                              selectedTileColor: Theme.of(context)\n                                  .colorScheme\n                                  .primaryContainer,\n                              title: Text(\n                                plugin.name,\n                                style: const TextStyle(\n                                    fontWeight: FontWeight.bold),\n                              ),\n                              subtitle: Column(\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: [\n                                  Row(\n                                    children: [\n                                      Text(\n                                        'Version: ${plugin.version}',\n                                        style:\n                                            const TextStyle(color: Colors.grey),\n                                      ),\n                                      if (canUpdate) ...[\n                                        const SizedBox(width: 8),\n                                        Container(\n                                          padding: const EdgeInsets.symmetric(\n                                              horizontal: 6, vertical: 2),\n                                          decoration: BoxDecoration(\n                                            color: Theme.of(context)\n                                                .colorScheme\n                                                .errorContainer,\n                                            borderRadius:\n                                                BorderRadius.circular(4),\n                                          ),\n                                          child: Text(\n                                            '可更新',\n                                            style: TextStyle(\n                                              fontSize: 12,\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .onErrorContainer,\n                                            ),\n                                          ),\n                                        ),\n                                      ],\n                                      if (pluginsController.validityTracker\n                                          .isSearchValid(plugin.name)) ...[\n                                        const SizedBox(width: 8),\n                                        Container(\n                                          padding: const EdgeInsets.symmetric(\n                                              horizontal: 6, vertical: 2),\n                                          decoration: BoxDecoration(\n                                            color: Theme.of(context)\n                                                .colorScheme\n                                                .tertiaryContainer,\n                                            borderRadius:\n                                                BorderRadius.circular(4),\n                                          ),\n                                          child: Text(\n                                            '搜索有效',\n                                            style: TextStyle(\n                                              fontSize: 12,\n                                              color: Theme.of(context)\n                                                  .colorScheme\n                                                  .onTertiaryContainer,\n                                            ),\n                                          ),\n                                        ),\n                                      ],\n                                    ],\n                                  ),\n                                ],\n                              ),\n                            ));\n                      });\n                });\n        }),\n      ),\n    );\n  }\n\n  Widget pluginCardTrailing(int index) {\n    final plugin = pluginsController.pluginList[index];\n    return Row(mainAxisSize: MainAxisSize.min, children: [\n      isMultiSelectMode\n          ? Checkbox(\n              value: selectedNames.contains(plugin.name),\n              onChanged: (bool? value) {\n                setState(() {\n                  if (value == true) {\n                    selectedNames.add(plugin.name);\n                  } else {\n                    selectedNames.remove(plugin.name);\n                    if (selectedNames.isEmpty) {\n                      isMultiSelectMode = false;\n                    }\n                  }\n                });\n              },\n            )\n          : popupMenuButton(index),\n      ReorderableDragStartListener(\n        index: index,\n        child: const Icon(Icons.drag_handle), // 单独的拖拽按钮\n      )\n    ]);\n  }\n\n  Widget popupMenuButton(int index) {\n    final plugin = pluginsController.pluginList[index];\n    return MenuAnchor(\n      consumeOutsideTap: true,\n      builder:\n          (BuildContext context, MenuController controller, Widget? child) {\n        return IconButton(\n          onPressed: () {\n            if (controller.isOpen) {\n              controller.close();\n            } else {\n              controller.open();\n            }\n          },\n          icon: const Icon(Icons.more_vert),\n        );\n      },\n      menuChildren: [\n        MenuItemButton(\n          requestFocusOnHover: false,\n          onPressed: () async {\n            var state = pluginsController.pluginUpdateStatus(plugin);\n            if (state == \"nonexistent\") {\n              KazumiDialog.showToast(message: '规则仓库中没有当前规则');\n            } else if (state == \"latest\") {\n              KazumiDialog.showToast(message: '规则已是最新');\n            } else if (state == \"updatable\") {\n              KazumiDialog.showLoading(msg: '更新中');\n              int res = await pluginsController.tryUpdatePlugin(plugin);\n              KazumiDialog.dismiss();\n              if (res == 0) {\n                KazumiDialog.showToast(message: '更新成功');\n              } else if (res == 1) {\n                KazumiDialog.showToast(message: 'kazumi版本过低, 此规则不兼容当前版本');\n              } else if (res == 2) {\n                KazumiDialog.showToast(message: '更新规则失败');\n              }\n            }\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                children: [\n                  Icon(Icons.update_rounded),\n                  SizedBox(width: 8),\n                  Text('更新'),\n                ],\n              ),\n            ),\n          ),\n        ),\n        MenuItemButton(\n          requestFocusOnHover: false,\n          onPressed: () {\n            Modular.to.pushNamed('/settings/plugin/editor', arguments: plugin);\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                children: [\n                  Icon(Icons.edit),\n                  SizedBox(width: 8),\n                  Text('编辑'),\n                ],\n              ),\n            ),\n          ),\n        ),\n        MenuItemButton(\n          requestFocusOnHover: false,\n          onPressed: () {\n            Modular.to.pushNamed('/settings/plugin/test', arguments: plugin);\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                children: [\n                  Icon(Icons.bug_report_outlined),\n                  SizedBox(width: 8),\n                  Text('测试'),\n                ],\n              ),\n            ),\n          ),\n        ),\n        MenuItemButton(\n          requestFocusOnHover: false,\n          onPressed: () {\n            KazumiDialog.show(builder: (context) {\n              return AlertDialog(\n                title: const Text('规则链接'),\n                content: SelectableText(\n                  Utils.jsonToKazumiBase64(json\n                      .encode(pluginsController.pluginList[index].toJson())),\n                  style: const TextStyle(fontWeight: FontWeight.bold),\n                  textAlign: TextAlign.center,\n                ),\n                actions: [\n                  TextButton(\n                    onPressed: () => KazumiDialog.dismiss(),\n                    child: Text(\n                      '取消',\n                      style: TextStyle(\n                          color: Theme.of(context).colorScheme.outline),\n                    ),\n                  ),\n                  TextButton(\n                    onPressed: () {\n                      Clipboard.setData(ClipboardData(\n                        text: Utils.jsonToKazumiBase64(\n                          json.encode(\n                            pluginsController.pluginList[index].toJson(),\n                          ),\n                        ),\n                      ));\n                      KazumiDialog.dismiss();\n                    },\n                    child: const Text('复制到剪贴板'),\n                  ),\n                ],\n              );\n            });\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                children: [\n                  Icon(Icons.share),\n                  SizedBox(width: 8),\n                  Text('分享'),\n                ],\n              ),\n            ),\n          ),\n        ),\n        MenuItemButton(\n          requestFocusOnHover: false,\n          onPressed: () async {\n            setState(() {\n              pluginsController.removePlugin(plugin);\n            });\n          },\n          child: Container(\n            height: 48,\n            constraints: BoxConstraints(minWidth: 112),\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Row(\n                children: [\n                  Icon(Icons.delete),\n                  SizedBox(width: 8),\n                  Text('删除'),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/popular/popular_controller.dart",
    "content": "import 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:mobx/mobx.dart';\n\npart 'popular_controller.g.dart';\n\nclass PopularController = _PopularController with _$PopularController;\n\nabstract class _PopularController with Store {\n  final ScrollController scrollController = ScrollController();\n\n  @observable\n  String currentTag = '';\n\n  @observable\n  ObservableList<BangumiItem> bangumiList = ObservableList.of([]);\n\n  @observable\n  ObservableList<BangumiItem> trendList = ObservableList.of([]);\n\n  double scrollOffset = 0.0;\n\n  @observable\n  bool isLoadingMore = false;\n\n  @observable\n  bool isTimeOut = false;\n\n  void setCurrentTag(String s) {\n    currentTag = s;\n  }\n\n  void clearBangumiList() {\n    bangumiList.clear();\n  }\n\n  Future<void> queryBangumiByTrend({String type = 'add'}) async {\n    if (type == 'init') {\n      trendList.clear();\n    }\n    isLoadingMore = true;\n    var result =\n        await BangumiHTTP.getBangumiTrendsList(offset: trendList.length);\n    trendList.addAll(result);\n    isLoadingMore = false;\n    isTimeOut = trendList.isEmpty;\n  }\n\n  Future<void> queryBangumiByTag({String type = 'add'}) async {\n    if (type == 'init') {\n      bangumiList.clear();\n    }\n    isLoadingMore = true;\n    int randomNumber = Random().nextInt(8000) + 1;\n    var tag = currentTag;\n    var result = await BangumiHTTP.getBangumiList(rank: randomNumber, tag: tag);\n    bangumiList.addAll(result);\n    isLoadingMore = false;\n    isTimeOut = bangumiList.isEmpty;\n  }\n}\n"
  },
  {
    "path": "lib/pages/popular/popular_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'popular_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$PopularController on _PopularController, Store {\n  late final _$currentTagAtom =\n      Atom(name: '_PopularController.currentTag', context: context);\n\n  @override\n  String get currentTag {\n    _$currentTagAtom.reportRead();\n    return super.currentTag;\n  }\n\n  @override\n  set currentTag(String value) {\n    _$currentTagAtom.reportWrite(value, super.currentTag, () {\n      super.currentTag = value;\n    });\n  }\n\n  late final _$bangumiListAtom =\n      Atom(name: '_PopularController.bangumiList', context: context);\n\n  @override\n  ObservableList<BangumiItem> get bangumiList {\n    _$bangumiListAtom.reportRead();\n    return super.bangumiList;\n  }\n\n  @override\n  set bangumiList(ObservableList<BangumiItem> value) {\n    _$bangumiListAtom.reportWrite(value, super.bangumiList, () {\n      super.bangumiList = value;\n    });\n  }\n\n  late final _$trendListAtom =\n      Atom(name: '_PopularController.trendList', context: context);\n\n  @override\n  ObservableList<BangumiItem> get trendList {\n    _$trendListAtom.reportRead();\n    return super.trendList;\n  }\n\n  @override\n  set trendList(ObservableList<BangumiItem> value) {\n    _$trendListAtom.reportWrite(value, super.trendList, () {\n      super.trendList = value;\n    });\n  }\n\n  late final _$isLoadingMoreAtom =\n      Atom(name: '_PopularController.isLoadingMore', context: context);\n\n  @override\n  bool get isLoadingMore {\n    _$isLoadingMoreAtom.reportRead();\n    return super.isLoadingMore;\n  }\n\n  @override\n  set isLoadingMore(bool value) {\n    _$isLoadingMoreAtom.reportWrite(value, super.isLoadingMore, () {\n      super.isLoadingMore = value;\n    });\n  }\n\n  late final _$isTimeOutAtom =\n      Atom(name: '_PopularController.isTimeOut', context: context);\n\n  @override\n  bool get isTimeOut {\n    _$isTimeOutAtom.reportRead();\n    return super.isTimeOut;\n  }\n\n  @override\n  set isTimeOut(bool value) {\n    _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () {\n      super.isTimeOut = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\ncurrentTag: ${currentTag},\nbangumiList: ${bangumiList},\ntrendList: ${trendList},\nisLoadingMore: ${isLoadingMore},\nisTimeOut: ${isTimeOut}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/popular/popular_module.dart",
    "content": "import 'package:kazumi/pages/popular/popular_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass PopularModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const PopularPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/popular/popular_page.dart",
    "content": "import 'dart:ui';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'package:kazumi/bean/widget/custom_dropdown_menu.dart';\nimport 'package:kazumi/pages/popular/popular_controller.dart';\nimport 'package:kazumi/bean/card/bangumi_card.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter/services.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/pages/menu/menu.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb;\n\nclass PopularPage extends StatefulWidget {\n  const PopularPage({super.key});\n\n  @override\n  State<PopularPage> createState() => _PopularPageState();\n}\n\nclass _PopularPageState extends State<PopularPage>\n    with AutomaticKeepAliveClientMixin {\n  DateTime? _lastPressedAt;\n  late NavigationBarState navigationBarState;\n  final FocusNode _focusNode = FocusNode();\n  final ScrollController scrollController = ScrollController();\n  final PopularController popularController = Modular.get<PopularController>();\n\n  // Key used to position the dropdown menu for the tag selector\n  final GlobalKey selectorKey = GlobalKey();\n\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController.addListener(scrollListener);\n    if (popularController.trendList.isEmpty) {\n      popularController.queryBangumiByTrend();\n    }\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n  }\n\n  @override\n  void dispose() {\n    _focusNode.dispose();\n    scrollController.removeListener(scrollListener);\n    super.dispose();\n  }\n\n  void scrollListener() {\n    popularController.scrollOffset = scrollController.offset;\n    if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200 &&\n        !popularController.isLoadingMore) {\n      KazumiLogger().i('PopularPageController: Fetching next recommendation batch');\n      if (popularController.currentTag != '') {\n        popularController.queryBangumiByTag();\n      } else {\n        popularController.queryBangumiByTrend();\n      }\n    }\n  }\n\n  bool showWindowButton() {\n    return GStorage.setting\n        .get(SettingBoxKey.showWindowButton, defaultValue: false);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n    if (_lastPressedAt == null ||\n        DateTime.now().difference(_lastPressedAt!) >\n            const Duration(seconds: 2)) {\n      _lastPressedAt = DateTime.now();\n      KazumiDialog.showToast(message: \"再按一次退出应用\", context: context);\n      return;\n    }\n    SystemNavigator.pop();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        body: CustomScrollView(\n          controller: scrollController,\n          slivers: [\n            buildSliverAppBar(),\n            SliverToBoxAdapter(\n              child: Observer(\n                builder: (_) => AnimatedOpacity(\n                  opacity: popularController.isLoadingMore ? 1.0 : 0.0,\n                  duration: const Duration(milliseconds: 300),\n                  child: popularController.isLoadingMore\n                      ? const LinearProgressIndicator(minHeight: 4)\n                      : const SizedBox(height: 4),\n                ),\n              ),\n            ),\n            SliverPadding(\n                padding: const EdgeInsets.fromLTRB(\n                    StyleString.cardSpace, 0, StyleString.cardSpace, 0),\n                sliver: Observer(builder: (_) {\n                  if (popularController.isTimeOut) {\n                    return SliverToBoxAdapter(\n                      child: SizedBox(\n                        height: 400,\n                        child: GeneralErrorWidget(\n                          errMsg: '什么都没有找到 (´;ω;`)',\n                          actions: [\n                            GeneralErrorButton(\n                              onPressed: () {\n                                if (popularController.trendList.isEmpty) {\n                                  popularController.queryBangumiByTrend();\n                                } else {\n                                  popularController.queryBangumiByTag();\n                                }\n                              },\n                              text: '点击重试',\n                            ),\n                          ],\n                        ),\n                      ),\n                    );\n                  }\n                  return contentGrid(\n                    (popularController.currentTag == '')\n                        ? popularController.trendList\n                        : popularController.bangumiList,\n                  );\n                })),\n          ],\n        ),\n        floatingActionButton: FloatingActionButton(\n          onPressed: () => scrollController.animateTo(0,\n              duration: const Duration(milliseconds: 350),\n              curve: Curves.easeOut),\n          child: const Icon(Icons.arrow_upward),\n        ),\n      ),\n    );\n  }\n\n  Widget contentGrid(bangumiList) {\n    int crossCount = 3;\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {\n      crossCount = 5;\n    }\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {\n      crossCount = 6;\n    }\n    return SliverPadding(\n      padding: const EdgeInsets.all(8),\n      sliver: SliverGrid(\n        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n          // 行间距\n          mainAxisSpacing: StyleString.cardSpace - 2,\n          // 列间距\n          crossAxisSpacing: StyleString.cardSpace,\n          // 列数\n          crossAxisCount: crossCount,\n          mainAxisExtent:\n              MediaQuery.of(context).size.width / crossCount / 0.65 +\n                  MediaQuery.textScalerOf(context).scale(32.0),\n        ),\n        delegate: SliverChildBuilderDelegate(\n          (BuildContext context, int index) {\n            return bangumiList!.isNotEmpty\n                ? BangumiCardV(bangumiItem: bangumiList[index])\n                : null;\n          },\n          childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,\n        ),\n      ),\n    );\n  }\n\n  Widget buildSliverAppBar() {\n    final theme = Theme.of(context);\n    return SliverAppBar(\n      pinned: true,\n      stretch: true,\n      expandedHeight: 120,\n      elevation: 0,\n      titleSpacing: 0,\n      centerTitle: false,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      actions: buildActions(),\n      title: null,\n      flexibleSpace: SafeArea(\n        child: dtb.DragToMoveArea(\n          child: LayoutBuilder(\n            builder: (context, constraints) {\n              final double maxExtent = 120 - MediaQuery.of(context).padding.top;\n              final t = (1 -\n                  ((constraints.maxHeight - kToolbarHeight) /\n                          (maxExtent - kToolbarHeight))\n                      .clamp(0.0, 1.0));\n              // 字重收缩后为 w500，展开时为 w700\n              final fontWeight = t < 0.5 ? FontWeight.w700 : FontWeight.w500;\n              final fontSize = lerpDouble(28, 20, t)!;\n              return Align(\n                alignment: Alignment.centerLeft,\n                child: Padding(\n                  padding: const EdgeInsets.only(\n                      left: 16, top: 8, bottom: 8, right: 60),\n                  child: SizedBox(\n                    height: 44,\n                    child: Observer(\n                      builder: (_) {\n                        final bool isTrend = popularController.currentTag == '';\n                        return InkWell(\n                          key: selectorKey,\n                          borderRadius: BorderRadius.circular(8),\n                          onTap: showTagMenu,\n                          child: Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              Text(\n                                isTrend ? '热门番组' : popularController.currentTag,\n                                style: theme.textTheme.headlineMedium!.copyWith(\n                                  fontWeight: fontWeight,\n                                  fontSize: fontSize,\n                                ),\n                              ),\n                              const SizedBox(width: 4),\n                              Icon(Icons.keyboard_arrow_down,\n                                  size: fontSize, color: theme.iconTheme.color),\n                            ],\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> buildActions() {\n    final actions = <Widget>[\n      if (MediaQuery.of(context).orientation == Orientation.portrait)\n        IconButton(\n          tooltip: '搜索',\n          onPressed: () => Modular.to.pushNamed('/search/'),\n          icon: const Icon(Icons.search),\n        ),\n    ];\n    actions.add(\n      IconButton(\n        tooltip: '历史记录',\n        onPressed: () => Modular.to.pushNamed('/settings/history/'),\n        icon: const Icon(Icons.history),\n      ),\n    );\n    if (Utils.isDesktop()) {\n      if (!showWindowButton()) {\n        actions.add(\n          IconButton(\n            tooltip: '退出',\n            onPressed: () => windowManager.close(),\n            icon: const Icon(Icons.close),\n          ),\n        );\n      }\n    }\n    return actions;\n  }\n\n  Future<void> showTagMenu() async {\n    // Calculate the position of the button manually to position the dropdown menu.\n    // Using CustomDropdownMenu instead of PopupMenuButton to avoid flickering issues\n    // and to support different font sizes in the button and menu items.\n    final RenderBox renderBox =\n        selectorKey.currentContext!.findRenderObject() as RenderBox;\n    final Offset offset = renderBox.localToGlobal(Offset.zero);\n    final Size size = renderBox.size;\n\n    final selected = await Navigator.push<String>(\n      context,\n      PageRouteBuilder(\n        opaque: false,\n        barrierDismissible: true,\n        barrierColor: Colors.transparent,\n        pageBuilder: (context, animation, secondaryAnimation) {\n          return CustomDropdownMenu(\n            offset: offset,\n            buttonSize: size,\n            animation: animation,\n            maxWidth: 80,\n            items: [\n              '',\n              ...defaultAnimeTags,\n            ],\n            itemBuilder: (item) => item.isEmpty ? '热门番组' : item,\n          );\n        },\n        transitionDuration: const Duration(milliseconds: 200),\n        reverseTransitionDuration: const Duration(milliseconds: 150),\n      ),\n    );\n\n    if (selected == null) return;\n    if (selected == '' && popularController.currentTag != '') {\n      scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 250), curve: Curves.easeOut);\n      popularController.setCurrentTag('');\n      popularController.clearBangumiList();\n      if (popularController.trendList.isEmpty) {\n        await popularController.queryBangumiByTrend();\n      }\n    } else if (selected != '' && selected != popularController.currentTag) {\n      scrollController.animateTo(0,\n          duration: const Duration(milliseconds: 250), curve: Curves.easeOut);\n      popularController.setCurrentTag(selected);\n      await popularController.queryBangumiByTag(type: 'init');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/router.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/popular/popular_module.dart';\nimport 'package:kazumi/pages/my/my_module.dart';\nimport 'package:kazumi/pages/timeline/timeline_module.dart';\nimport 'package:kazumi/pages/collect/collect_module.dart';\n\nclass MenuRouteItem {\n  final String path;\n  final Module module;\n\n  const MenuRouteItem({\n    required this.path,\n    required this.module,\n  });\n}\n\nclass MenuRoute {\n  final List<MenuRouteItem> menuList;\n\n  const MenuRoute(this.menuList);\n\n  int get size => menuList.length;\n\n  List<Module> get moduleList {\n    return menuList.map((e) => e.module).toList();\n  }\n\n  List<ModuleRoute> get routes {\n    return menuList.map((e) => ModuleRoute(e.path, module: e.module)).toList();\n  }\n\n  getPath(int index) {\n    return menuList[index].path;\n  }\n}\n\nfinal MenuRoute menu = MenuRoute([\n  MenuRouteItem(\n    path: \"/popular\",\n    module: PopularModule(),\n  ),\n  MenuRouteItem(\n    path: \"/timeline\",\n    module: TimelineModule(),\n  ),\n  MenuRouteItem(\n    path: \"/collect\",\n    module: CollectModule(),\n  ),\n  MenuRouteItem(\n    path: \"/my\",\n    module: MyModule(),\n  ),\n]);\n"
  },
  {
    "path": "lib/pages/search/search_controller.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:kazumi/utils/search_parser.dart';\nimport 'package:kazumi/modules/search/search_history_module.dart';\nimport 'package:kazumi/repositories/collect_repository.dart';\nimport 'package:kazumi/repositories/search_history_repository.dart';\nimport 'package:kazumi/modules/collect/collect_type.dart';\n\npart 'search_controller.g.dart';\n\nclass SearchPageController = _SearchPageController with _$SearchPageController;\n\nabstract class _SearchPageController with Store {\n  final _collectRepository = Modular.get<ICollectRepository>();\n  final _searchHistoryRepository = Modular.get<ISearchHistoryRepository>();\n\n  @observable\n  bool isLoading = false;\n\n  @observable\n  bool isTimeOut = false;\n\n  @observable\n  late bool notShowWatchedBangumis = _collectRepository.getSearchNotShowWatchedBangumis();\n\n  @observable\n  late bool notShowAbandonedBangumis = _collectRepository.getSearchNotShowAbandonedBangumis();\n\n  @observable\n  ObservableList<BangumiItem> bangumiList = ObservableList.of([]);\n\n  @observable\n  ObservableList<SearchHistory> searchHistories = ObservableList.of([]);\n\n  @action\n  void loadSearchHistories() {\n    final histories = _searchHistoryRepository.getAllHistories();\n    searchHistories.clear();\n    searchHistories.addAll(histories);\n  }\n\n  /// Avaliable sort parameters:\n  /// 1. heat\n  /// 2. match\n  /// 3. rank\n  /// 4. score\n  String attachSortParams(String input, String sort) {\n    SearchParser parser = SearchParser(input);\n    String newInput = parser.updateSort(sort);\n    return newInput;\n  }\n\n  @action\n  Future<void> searchBangumi(String input, {String type = 'add'}) async {\n    if (type != 'add') {\n      bangumiList.clear();\n      bool privateMode = _collectRepository.getPrivateMode();\n      if (!privateMode) {\n        // 检查是否已满，删除最旧的记录\n        if (_searchHistoryRepository.isHistoryFull(10)) {\n          await _searchHistoryRepository.deleteOldest();\n        }\n        // 删除重复的历史记录\n        await _searchHistoryRepository.deleteDuplicates(input);\n        // 保存新的搜索历史\n        await _searchHistoryRepository.saveHistory(input);\n        // 重新加载历史记录\n        loadSearchHistories();\n      }\n    }\n    isLoading = true;\n    isTimeOut = false;\n    SearchParser parser = SearchParser(input);\n    String? idString = parser.parseId();\n    String? tag = parser.parseTag();\n    String? sort = parser.parseSort();\n    String keywords = parser.parseKeywords();\n    if (idString != null) {\n      final id = int.tryParse(idString);\n      if (id != null) {\n        final BangumiItem? item = await BangumiHTTP.getBangumiInfoByID(id);\n        if (item != null) {\n          bangumiList.add(item);\n        }\n        return;\n      }\n    }\n    var result = await BangumiHTTP.bangumiSearch(keywords,\n        tags: [if (tag != null) tag],\n        offset: bangumiList.length,\n        sort: sort ?? 'heat');\n    bangumiList.addAll(result);\n    isLoading = false;\n    isTimeOut = bangumiList.isEmpty;\n  }\n\n  @action\n  Future<void> deleteSearchHistory(SearchHistory history) async {\n    await _searchHistoryRepository.deleteHistory(history);\n    loadSearchHistories();\n  }\n\n  @action\n  Future<void> clearSearchHistory() async {\n    await _searchHistoryRepository.clearAllHistories();\n    loadSearchHistories();\n  }\n\n  @action\n  Future<void> setNotShowWatchedBangumis(bool value) async {\n    notShowWatchedBangumis = value;\n    await _collectRepository.updateSearchNotShowWatchedBangumis(value);\n  }\n\n  @action\n  Future<void> setNotShowAbandonedBangumis(bool value) async {\n    notShowAbandonedBangumis = value;\n    await _collectRepository.updateSearchNotShowAbandonedBangumis(value);\n  }\n\n  Set<int> loadWatchedBangumiIds() {\n    return _collectRepository.getBangumiIdsByType(CollectType.watched);\n  }\n\n  Set<int> loadAbandonedBangumiIds() {\n    return _collectRepository.getBangumiIdsByType(CollectType.abandoned);\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/search_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'search_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$SearchPageController on _SearchPageController, Store {\n  late final _$isLoadingAtom =\n      Atom(name: '_SearchPageController.isLoading', context: context);\n\n  @override\n  bool get isLoading {\n    _$isLoadingAtom.reportRead();\n    return super.isLoading;\n  }\n\n  @override\n  set isLoading(bool value) {\n    _$isLoadingAtom.reportWrite(value, super.isLoading, () {\n      super.isLoading = value;\n    });\n  }\n\n  late final _$isTimeOutAtom =\n      Atom(name: '_SearchPageController.isTimeOut', context: context);\n\n  @override\n  bool get isTimeOut {\n    _$isTimeOutAtom.reportRead();\n    return super.isTimeOut;\n  }\n\n  @override\n  set isTimeOut(bool value) {\n    _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () {\n      super.isTimeOut = value;\n    });\n  }\n\n  late final _$notShowWatchedBangumisAtom = Atom(\n      name: '_SearchPageController.notShowWatchedBangumis', context: context);\n\n  @override\n  bool get notShowWatchedBangumis {\n    _$notShowWatchedBangumisAtom.reportRead();\n    return super.notShowWatchedBangumis;\n  }\n\n  bool _notShowWatchedBangumisIsInitialized = false;\n\n  @override\n  set notShowWatchedBangumis(bool value) {\n    _$notShowWatchedBangumisAtom.reportWrite(\n        value,\n        _notShowWatchedBangumisIsInitialized\n            ? super.notShowWatchedBangumis\n            : null, () {\n      super.notShowWatchedBangumis = value;\n      _notShowWatchedBangumisIsInitialized = true;\n    });\n  }\n\n  late final _$notShowAbandonedBangumisAtom = Atom(\n      name: '_SearchPageController.notShowAbandonedBangumis', context: context);\n\n  @override\n  bool get notShowAbandonedBangumis {\n    _$notShowAbandonedBangumisAtom.reportRead();\n    return super.notShowAbandonedBangumis;\n  }\n\n  bool _notShowAbandonedBangumisIsInitialized = false;\n\n  @override\n  set notShowAbandonedBangumis(bool value) {\n    _$notShowAbandonedBangumisAtom.reportWrite(\n        value,\n        _notShowAbandonedBangumisIsInitialized\n            ? super.notShowAbandonedBangumis\n            : null, () {\n      super.notShowAbandonedBangumis = value;\n      _notShowAbandonedBangumisIsInitialized = true;\n    });\n  }\n\n  late final _$bangumiListAtom =\n      Atom(name: '_SearchPageController.bangumiList', context: context);\n\n  @override\n  ObservableList<BangumiItem> get bangumiList {\n    _$bangumiListAtom.reportRead();\n    return super.bangumiList;\n  }\n\n  @override\n  set bangumiList(ObservableList<BangumiItem> value) {\n    _$bangumiListAtom.reportWrite(value, super.bangumiList, () {\n      super.bangumiList = value;\n    });\n  }\n\n  late final _$searchHistoriesAtom =\n      Atom(name: '_SearchPageController.searchHistories', context: context);\n\n  @override\n  ObservableList<SearchHistory> get searchHistories {\n    _$searchHistoriesAtom.reportRead();\n    return super.searchHistories;\n  }\n\n  @override\n  set searchHistories(ObservableList<SearchHistory> value) {\n    _$searchHistoriesAtom.reportWrite(value, super.searchHistories, () {\n      super.searchHistories = value;\n    });\n  }\n\n  late final _$searchBangumiAsyncAction =\n      AsyncAction('_SearchPageController.searchBangumi', context: context);\n\n  @override\n  Future<void> searchBangumi(String input, {String type = 'add'}) {\n    return _$searchBangumiAsyncAction\n        .run(() => super.searchBangumi(input, type: type));\n  }\n\n  late final _$deleteSearchHistoryAsyncAction = AsyncAction(\n      '_SearchPageController.deleteSearchHistory',\n      context: context);\n\n  @override\n  Future<void> deleteSearchHistory(SearchHistory history) {\n    return _$deleteSearchHistoryAsyncAction\n        .run(() => super.deleteSearchHistory(history));\n  }\n\n  late final _$clearSearchHistoryAsyncAction =\n      AsyncAction('_SearchPageController.clearSearchHistory', context: context);\n\n  @override\n  Future<void> clearSearchHistory() {\n    return _$clearSearchHistoryAsyncAction\n        .run(() => super.clearSearchHistory());\n  }\n\n  late final _$setNotShowWatchedBangumisAsyncAction = AsyncAction(\n      '_SearchPageController.setNotShowWatchedBangumis',\n      context: context);\n\n  @override\n  Future<void> setNotShowWatchedBangumis(bool value) {\n    return _$setNotShowWatchedBangumisAsyncAction\n        .run(() => super.setNotShowWatchedBangumis(value));\n  }\n\n  late final _$setNotShowAbandonedBangumisAsyncAction = AsyncAction(\n      '_SearchPageController.setNotShowAbandonedBangumis',\n      context: context);\n\n  @override\n  Future<void> setNotShowAbandonedBangumis(bool value) {\n    return _$setNotShowAbandonedBangumisAsyncAction\n        .run(() => super.setNotShowAbandonedBangumis(value));\n  }\n\n  late final _$_SearchPageControllerActionController =\n      ActionController(name: '_SearchPageController', context: context);\n\n  @override\n  void loadSearchHistories() {\n    final _$actionInfo = _$_SearchPageControllerActionController.startAction(\n        name: '_SearchPageController.loadSearchHistories');\n    try {\n      return super.loadSearchHistories();\n    } finally {\n      _$_SearchPageControllerActionController.endAction(_$actionInfo);\n    }\n  }\n\n  @override\n  String toString() {\n    return '''\nisLoading: ${isLoading},\nisTimeOut: ${isTimeOut},\nnotShowWatchedBangumis: ${notShowWatchedBangumis},\nnotShowAbandonedBangumis: ${notShowAbandonedBangumis},\nbangumiList: ${bangumiList},\nsearchHistories: ${searchHistories}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/search_module.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/search/search_page.dart';\n\nclass SearchModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/:tag\", child: (_) {\n      return SearchPage(inputTag: r.args.params['tag']);\n    });\n  }\n}\n"
  },
  {
    "path": "lib/pages/search/search_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/bean/card/bangumi_card.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\nimport 'package:kazumi/pages/search/search_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass SearchPage extends StatefulWidget {\n  const SearchPage({super.key, this.inputTag = ''});\n\n  final String inputTag;\n\n  @override\n  State<SearchPage> createState() => _SearchPageState();\n}\n\nclass _SearchPageState extends State<SearchPage> {\n  final SearchController searchController = SearchController();\n\n  /// Don't use modular singleton here. We may have multiple search pages.\n  /// Use a new instance of SearchPageController for each search page.\n  final SearchPageController searchPageController = SearchPageController();\n  final ScrollController scrollController = ScrollController();\n\n  final List<Tab> tabs = [\n    Tab(text: \"排序方式\"),\n    Tab(text: \"过滤器\"),\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController.addListener(scrollListener);\n    searchPageController.loadSearchHistories();\n  }\n\n  @override\n  void dispose() {\n    searchPageController.bangumiList.clear();\n    scrollController.removeListener(scrollListener);\n    super.dispose();\n  }\n\n  void scrollListener() {\n    if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent - 200 &&\n        !searchPageController.isLoading &&\n        searchController.text != '' &&\n        searchPageController.bangumiList.length >= 20) {\n      KazumiLogger().i('SearchController: search results is loading more');\n      searchPageController.searchBangumi(searchController.text, type: 'add');\n    }\n  }\n\n  Widget showFilterSwitcher() {\n    return Wrap(\n      children: [\n        Observer(\n          builder: (context) => InkWell(\n            onTap: () {\n              searchPageController.setNotShowWatchedBangumis(\n                  !searchPageController.notShowWatchedBangumis);\n            },\n            child: ListTile(\n              title: const Text('不显示已看过的番剧'),\n              trailing: Switch(\n                value: searchPageController.notShowWatchedBangumis,\n                onChanged: (value) {\n                  searchPageController.setNotShowWatchedBangumis(value);\n                },\n              ),\n            ),\n          ),\n        ),\n        Observer(\n          builder: (context) => InkWell(\n            onTap: () {\n              searchPageController.setNotShowAbandonedBangumis(\n                  !searchPageController.notShowAbandonedBangumis);\n            },\n            child: ListTile(\n              title: const Text('不显示已抛弃的番剧'),\n              trailing: Switch(\n                value: searchPageController.notShowAbandonedBangumis,\n                onChanged: (value) {\n                  searchPageController.setNotShowAbandonedBangumis(value);\n                },\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget showSortSwitcher() {\n    return Wrap(\n      children: [\n        Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            ListTile(\n              title: const Text('按热度排序'),\n              onTap: () {\n                Navigator.pop(context);\n                searchController.text = searchPageController.attachSortParams(\n                    searchController.text, 'heat');\n                searchPageController.searchBangumi(searchController.text,\n                    type: 'init');\n              },\n            ),\n            ListTile(\n              title: const Text('按评分排序'),\n              onTap: () {\n                Navigator.pop(context);\n                searchController.text = searchPageController.attachSortParams(\n                    searchController.text, 'rank');\n                searchPageController.searchBangumi(searchController.text,\n                    type: 'init');\n              },\n            ),\n            ListTile(\n              title: const Text('按匹配程度排序'),\n              onTap: () {\n                Navigator.pop(context);\n                searchController.text = searchPageController.attachSortParams(\n                    searchController.text, 'match');\n                searchPageController.searchBangumi(searchController.text,\n                    type: 'init');\n              },\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget showSearchOptionTabBar({required List<Widget> options}) {\n    return DefaultTabController(\n        length: tabs.length,\n        child: Scaffold(\n            body: Column(\n          children: [\n            PreferredSize(\n              preferredSize: Size.fromHeight(kToolbarHeight),\n              child: Material(\n                child: TabBar(\n                  tabs: tabs,\n                ),\n              ),\n            ),\n            Expanded(\n                child: TabBarView(\n              children: options,\n            ))\n          ],\n        )));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (widget.inputTag != '') {\n        final String tagString = 'tag:${Uri.decodeComponent(widget.inputTag)}';\n        searchController.text = tagString;\n        searchPageController.searchBangumi(tagString, type: 'init');\n      }\n    });\n    return Scaffold(\n      appBar: SysAppBar(\n        backgroundColor: Colors.transparent,\n        title: const Text(\"搜索\"),\n      ),\n      floatingActionButton: FloatingActionButton.extended(\n        onPressed: () async {\n          showModalBottomSheet(\n            isScrollControlled: true,\n            constraints: BoxConstraints(\n              maxHeight: (MediaQuery.sizeOf(context).height >=\n                      LayoutBreakpoint.compact['height']!)\n                  ? MediaQuery.of(context).size.height * 1 / 4\n                  : MediaQuery.of(context).size.height,\n              maxWidth: (MediaQuery.sizeOf(context).width >=\n                      LayoutBreakpoint.medium['width']!)\n                  ? MediaQuery.of(context).size.width * 9 / 16\n                  : MediaQuery.of(context).size.width,\n            ),\n            clipBehavior: Clip.antiAlias,\n            backgroundColor: Theme.of(context).scaffoldBackgroundColor,\n            context: context,\n            builder: (context) {\n              return showSearchOptionTabBar(\n                  options: [showSortSwitcher(), showFilterSwitcher()]);\n            },\n          );\n        },\n        icon: const Icon(Icons.sort),\n        label: const Text(\"搜索设置\"),\n      ),\n      body: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(8, 0, 8, 16),\n            child: FocusScope(\n              descendantsAreFocusable: false,\n              child: SearchAnchor.bar(\n                searchController: searchController,\n                barElevation: WidgetStateProperty<double>.fromMap(\n                  <WidgetStatesConstraint, double>{WidgetState.any: 0},\n                ),\n                viewElevation: 0,\n                viewLeading: IconButton(\n                  onPressed: () {\n                    Navigator.of(context).pop();\n                  },\n                  icon: Icon(Icons.arrow_back),\n                ),\n                isFullScreen: MediaQuery.sizeOf(context).width <\n                    LayoutBreakpoint.compact['width']!,\n                suggestionsBuilder: (context, controller) => [\n                  Observer(\n                    builder: (context) {\n                      if (controller.text.isNotEmpty) {\n                        return Container(\n                          height: 400,\n                          alignment: Alignment.center,\n                          child: Text(\"无可用搜索建议，回车以直接检索\"),\n                        );\n                      } else {\n                        return Column(\n                          mainAxisSize: MainAxisSize.min,\n                          children: [\n                            for (var history in searchPageController\n                                .searchHistories\n                                .take(10))\n                              ListTile(\n                                title: Text(history.keyword),\n                                onTap: () {\n                                  controller.text = history.keyword;\n                                  searchPageController.searchBangumi(\n                                      controller.text,\n                                      type: 'init');\n                                  if (searchController.isOpen) {\n                                    searchController.closeView(history.keyword);\n                                  }\n                                },\n                                trailing: IconButton(\n                                  icon: const Icon(Icons.close),\n                                  onPressed: () {\n                                    searchPageController\n                                        .deleteSearchHistory(history);\n                                  },\n                                ),\n                              ),\n                          ],\n                        );\n                      }\n                    },\n                  ),\n                ],\n                onSubmitted: (value) {\n                  searchPageController.searchBangumi(value, type: 'init');\n                  if (searchController.isOpen) {\n                    searchController.closeView(value);\n                  }\n                },\n              ),\n            ),\n          ),\n          Expanded(\n            child: Observer(builder: (context) {\n              if (searchPageController.isTimeOut) {\n                return Center(\n                  child: SizedBox(\n                    height: 400,\n                    child: GeneralErrorWidget(\n                      errMsg: '什么都没有找到 (´;ω;`)',\n                      actions: [\n                        GeneralErrorButton(\n                          onPressed: () {\n                            searchPageController.searchBangumi(\n                                searchController.text,\n                                type: 'init');\n                          },\n                          text: '点击重试',\n                        ),\n                      ],\n                    ),\n                  ),\n                );\n              }\n\n\n              if (searchPageController.isLoading &&\n                  searchPageController.bangumiList.isEmpty) {\n                return Center(child: CircularProgressIndicator());\n              }\n              int crossCount = 3;\n              if (MediaQuery.sizeOf(context).width >\n                  LayoutBreakpoint.compact['width']!) {\n                crossCount = 5;\n              }\n              if (MediaQuery.sizeOf(context).width >\n                  LayoutBreakpoint.medium['width']!) {\n                crossCount = 6;\n              }\n              List<BangumiItem> filteredList = searchPageController.bangumiList.toList();\n\n              if (searchPageController.notShowWatchedBangumis) {\n                final watchedBangumiIds = searchPageController.loadWatchedBangumiIds();\n                filteredList = filteredList\n                    .where((item) => !watchedBangumiIds.contains(item.id))\n                    .toList();\n              }\n\n              if (searchPageController.notShowAbandonedBangumis) {\n                final abandonedBangumiIds = searchPageController.loadAbandonedBangumiIds();\n                filteredList = filteredList\n                    .where((item) => !abandonedBangumiIds.contains(item.id))\n                    .toList();\n              }\n\n              return GridView.builder(\n                controller: scrollController,\n                padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),\n                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n                  mainAxisSpacing: StyleString.cardSpace - 2,\n                  crossAxisSpacing: StyleString.cardSpace,\n                  crossAxisCount: crossCount,\n                  mainAxisExtent:\n                      MediaQuery.of(context).size.width / crossCount / 0.65 +\n                          MediaQuery.textScalerOf(context).scale(32.0),\n                ),\n                itemCount: filteredList.isNotEmpty ? filteredList.length : 10,\n                itemBuilder: (context, index) {\n                  return filteredList.isNotEmpty\n                      ? BangumiCardV(\n                          enableHero: false,\n                          bangumiItem: filteredList[index],\n                        )\n                      : Container();\n                },\n              );\n            }),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/danmaku/danmaku_module.dart",
    "content": "import 'package:kazumi/pages/settings/danmaku/danmaku_settings.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/settings/danmaku/danmaku_shield_settings.dart';\n\nclass DanmakuModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const DanmakuSettingsPage());\n    r.child(\"/shield\", child: (_) => const DanmakuShieldSettings());\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/danmaku/danmaku_settings.dart",
    "content": "import 'package:kazumi/utils/utils.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/pages/popular/popular_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass DanmakuSettingsPage extends StatefulWidget {\n  const DanmakuSettingsPage({super.key});\n\n  @override\n  State<DanmakuSettingsPage> createState() => _DanmakuSettingsPageState();\n}\n\nclass _DanmakuSettingsPageState extends State<DanmakuSettingsPage> {\n  Box setting = GStorage.setting;\n  late dynamic defaultDanmakuArea;\n  late dynamic defaultDanmakuOpacity;\n  late dynamic defaultDanmakuFontSize;\n  late int defaultDanmakuFontWeight;\n  late double defaultDanmakuDuration;\n  late double defaultDanmakuLineHeight;\n  late double defaultdanmakuBorderSize;\n  final PopularController popularController = Modular.get<PopularController>();\n  late bool danmakuBorder;\n  late bool danmakuTop;\n  late bool danmakuBottom;\n  late bool danmakuScroll;\n  late bool danmakuColor;\n  late bool danmakuMassive;\n  late bool danmakuDeduplication;\n  late bool danmakuBiliBiliSource;\n  late bool danmakuGamerSource;\n  late bool danmakuDanDanSource;\n  late bool danmakuFollowSpeed;\n\n  @override\n  void initState() {\n    super.initState();\n    defaultDanmakuArea =\n        setting.get(SettingBoxKey.danmakuArea, defaultValue: 1.0);\n    defaultDanmakuOpacity =\n        setting.get(SettingBoxKey.danmakuOpacity, defaultValue: 1.0);\n    defaultDanmakuFontSize = setting.get(SettingBoxKey.danmakuFontSize,\n        defaultValue: (Utils.isCompact()) ? 16.0 : 25.0);\n    defaultDanmakuFontWeight =\n        setting.get(SettingBoxKey.danmakuFontWeight, defaultValue: 4);\n    defaultDanmakuDuration =\n        setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0);\n    defaultDanmakuLineHeight =\n        setting.get(SettingBoxKey.danmakuLineHeight, defaultValue: 1.6);\n    danmakuBorder =\n        setting.get(SettingBoxKey.danmakuBorder, defaultValue: true);\n    defaultdanmakuBorderSize = \n        setting.get(SettingBoxKey.danmakuBorderSize, defaultValue: 1.5);\n    danmakuTop = setting.get(SettingBoxKey.danmakuTop, defaultValue: true);\n    danmakuBottom =\n        setting.get(SettingBoxKey.danmakuBottom, defaultValue: false);\n    danmakuScroll =\n        setting.get(SettingBoxKey.danmakuScroll, defaultValue: true);\n    danmakuColor = setting.get(SettingBoxKey.danmakuColor, defaultValue: true);\n    danmakuMassive =\n        setting.get(SettingBoxKey.danmakuMassive, defaultValue: false);\n    danmakuDeduplication = \n        setting.get(SettingBoxKey.danmakuDeduplication, defaultValue: false);\n    danmakuBiliBiliSource =\n        setting.get(SettingBoxKey.danmakuBiliBiliSource, defaultValue: true);\n    danmakuGamerSource =\n        setting.get(SettingBoxKey.danmakuGamerSource, defaultValue: true);\n    danmakuDanDanSource =\n        setting.get(SettingBoxKey.danmakuDanDanSource, defaultValue: true);\n    danmakuFollowSpeed =\n        setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  void updateDanmakuArea(double i) async {\n    await setting.put(SettingBoxKey.danmakuArea, i);\n    setState(() {\n      defaultDanmakuArea = i;\n    });\n  }\n\n  void updateDanmakuOpacity(double i) async {\n    await setting.put(SettingBoxKey.danmakuOpacity, i);\n    setState(() {\n      defaultDanmakuOpacity = i;\n    });\n  }\n\n  void updateDanmakuFontSize(double i) async {\n    await setting.put(SettingBoxKey.danmakuFontSize, i);\n    setState(() {\n      defaultDanmakuFontSize = i;\n    });\n  }\n\n  void updateDanmakuDuration(double i) async {\n    await setting.put(SettingBoxKey.danmakuDuration, i);\n    setState(() {\n      defaultDanmakuDuration = i;\n    });\n  }\n\n  void updateDanmakuLineHeight(double i) async {\n    await setting.put(SettingBoxKey.danmakuLineHeight, i);\n    setState(() {\n      defaultDanmakuLineHeight = i;\n    });\n  }\n\n  void updateDanmakuFontWeight(int i) async {\n    await setting.put(SettingBoxKey.danmakuFontWeight, i);\n    setState(() {\n      defaultDanmakuFontWeight = i;\n    });\n  }\n\n  void updateDanmakuBorderSize(double i) async {\n    await setting.put(SettingBoxKey.danmakuBorderSize, i);\n    setState(() {\n      defaultdanmakuBorderSize = i;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('弹幕设置')),\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuBiliBiliSource = value ?? !danmakuBiliBiliSource;\n                    await setting.put(SettingBoxKey.danmakuBiliBiliSource,\n                        danmakuBiliBiliSource);\n                    setState(() {});\n                  },\n                  title: Text('BiliBili', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuBiliBiliSource,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuGamerSource = value ?? !danmakuGamerSource;\n                    await setting.put(\n                        SettingBoxKey.danmakuGamerSource, danmakuGamerSource);\n                    setState(() {});\n                  },\n                  title: Text('Gamer', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuGamerSource,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuDanDanSource = value ?? !danmakuDanDanSource;\n                    await setting.put(\n                        SettingBoxKey.danmakuDanDanSource, danmakuDanDanSource);\n                    setState(() {});\n                  },\n                  title: Text('DanDan', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuDanDanSource,\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    Modular.to.pushNamed('/settings/danmaku/shield');\n                  },\n                  title: Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile(\n                  title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuArea,\n                    min: 0,\n                    max: 1,\n                    divisions: 8,\n                    label: '${(defaultDanmakuArea * 100).round()}%',\n                    onChanged: (value) {\n                      updateDanmakuArea(value);\n                    },\n                  ),\n                ),\n                SettingsTile(\n                  title: Text('弹幕持续时间', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuDuration,\n                    min: 2,\n                    max: 16,\n                    divisions: 14,\n                    label: '${defaultDanmakuDuration.round()}',\n                    onChanged: (value) {\n                      updateDanmakuDuration(value.round().toDouble());\n                    },\n                  ),\n                ),\n                SettingsTile(\n                  title: Text('弹幕行高', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuLineHeight,\n                    min: 0,\n                    max: 3,\n                    divisions: 30,\n                    label: defaultDanmakuLineHeight.toStringAsFixed(1),\n                    onChanged: (value) {\n                      updateDanmakuLineHeight(double.parse(value.toStringAsFixed(1)));\n                    },\n                  ),\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuFollowSpeed = value ?? !danmakuFollowSpeed;\n                    await setting.put(\n                        SettingBoxKey.danmakuFollowSpeed, danmakuFollowSpeed);\n                    setState(() {});\n                  },\n                  title: Text('弹幕跟随视频倍速', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('开启后弹幕速度会随视频倍速而改变', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuFollowSpeed,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuTop = value ?? !danmakuTop;\n                    await setting.put(SettingBoxKey.danmakuTop, danmakuTop);\n                    setState(() {});\n                  },\n                  title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuTop,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuBottom = value ?? !danmakuBottom;\n                    await setting.put(\n                        SettingBoxKey.danmakuBottom, danmakuBottom);\n                    setState(() {});\n                  },\n                  title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuBottom,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuScroll = value ?? !danmakuScroll;\n                    await setting.put(\n                        SettingBoxKey.danmakuScroll, danmakuScroll);\n                    setState(() {});\n                  },\n                  title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuScroll,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuMassive = value ?? !danmakuMassive;\n                    await setting.put(\n                        SettingBoxKey.danmakuMassive, danmakuMassive);\n                    setState(() {});\n                  },\n                  title: Text('海量弹幕', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('弹幕过多时进行叠加绘制', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuMassive,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuDeduplication = value ?? !danmakuDeduplication;\n                    await setting.put(\n                        SettingBoxKey.danmakuDeduplication, danmakuDeduplication);\n                    setState(() {});\n                  },\n                  title: Text('弹幕去重', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('相同内容弹幕过多时合并为一条弹幕', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuDeduplication,\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuBorder = value ?? !danmakuBorder;\n                    await setting.put(\n                        SettingBoxKey.danmakuBorder, danmakuBorder);\n                    setState(() {});\n                  },\n                  title: Text('弹幕描边', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuBorder,\n                ),\n                SettingsTile(\n                  title: Text('弹幕描边粗细', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultdanmakuBorderSize,\n                    min: 0.1,\n                    max: 3,\n                    divisions: 29,\n                    label: defaultdanmakuBorderSize.toStringAsFixed(1),\n                    onChanged: (value) {\n                      updateDanmakuBorderSize(double.parse(value.toStringAsFixed(1)));\n                    },\n                  ),\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    danmakuColor = value ?? !danmakuColor;\n                    await setting.put(SettingBoxKey.danmakuColor, danmakuColor);\n                    setState(() {});\n                  },\n                  title: Text('弹幕颜色', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: danmakuColor,\n                ),\n                SettingsTile(\n                  title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuFontSize,\n                    min: 10,\n                    max: Utils.isCompact() ? 32 : 48,\n                    label: '${defaultDanmakuFontSize.floorToDouble()}',\n                    onChanged: (value) {\n                      updateDanmakuFontSize(value.floorToDouble());\n                    },\n                  ),\n                ),\n                SettingsTile(\n                  title: Text('字体字重', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuFontWeight.toDouble(),\n                    min: 1,\n                    max: 9,\n                    divisions: 8,\n                    label: '$defaultDanmakuFontWeight',\n                    onChanged: (value) {\n                      updateDanmakuFontWeight(value.toInt());\n                    },\n                  ),\n                ),\n                SettingsTile(\n                  title: Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultDanmakuOpacity,\n                    min: 0.1,\n                    max: 1,\n                    label: '${(defaultDanmakuOpacity * 100).round()}%',\n                    onChanged: (value) {\n                      updateDanmakuOpacity(\n                          double.parse(value.toStringAsFixed(2)));\n                    },\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/danmaku/danmaku_settings_sheet.dart",
    "content": "import 'package:canvas_danmaku/canvas_danmaku.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/pages/settings/danmaku/danmaku_shield_settings.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass DanmakuSettingsSheet extends StatefulWidget {\n  final DanmakuController danmakuController;\n  final VoidCallback? onUpdateDanmakuSpeed;\n\n  const DanmakuSettingsSheet({\n    super.key,\n    required this.danmakuController,\n    this.onUpdateDanmakuSpeed,\n  });\n\n  @override\n  State<DanmakuSettingsSheet> createState() => _DanmakuSettingsSheetState();\n}\n\nclass _DanmakuSettingsSheetState extends State<DanmakuSettingsSheet> {\n  Box setting = GStorage.setting;\n\n  void showDanmakuShieldSheet() {\n    showModalBottomSheet(\n        isScrollControlled: true,\n        constraints: BoxConstraints(\n            maxHeight: MediaQuery.of(context).size.height * 3 / 4,\n            maxWidth: (Utils.isDesktop() || Utils.isTablet())\n                ? MediaQuery.of(context).size.width * 9 / 16\n                : MediaQuery.of(context).size.width),\n        clipBehavior: Clip.antiAlias,\n        context: context,\n        builder: (context) {\n          return SafeArea(\n            bottom: false,\n            child: DanmakuShieldSettings(),\n          );\n        });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return SafeArea(\n      bottom: false,\n      child: SettingsList(\n        sections: [\n          SettingsSection(\n            title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile.navigation(\n                onPressed: (_) {\n                  showDanmakuShieldSheet();\n                },\n                title: Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)),\n              ),\n            ],\n          ),\n          SettingsSection(\n            title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile(\n                title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)),\n                description: Slider(\n                  value: widget.danmakuController.option.fontSize,\n                  min: 10,\n                  max: Utils.isCompact() ? 32 : 48,\n                  label:\n                      '${widget.danmakuController.option.fontSize.floorToDouble()}',\n                  onChanged: (value) {\n                    setState(() => widget.danmakuController.updateOption(\n                          widget.danmakuController.option.copyWith(\n                            fontSize: value.floorToDouble(),\n                          ),\n                        ));\n                    setting.put(\n                        SettingBoxKey.danmakuFontSize, value.floorToDouble());\n                  },\n                ),\n              ),\n              SettingsTile(\n                title: Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)),\n                description: Slider(\n                  value: widget.danmakuController.option.opacity,\n                  min: 0.1,\n                  max: 1,\n                  label:\n                      '${(widget.danmakuController.option.opacity * 100).round()}%',\n                  onChanged: (value) {\n                    setState(() => widget.danmakuController.updateOption(\n                          widget.danmakuController.option.copyWith(\n                            opacity: value,\n                          ),\n                        ));\n                    setting.put(SettingBoxKey.danmakuOpacity,\n                        double.parse(value.toStringAsFixed(2)));\n                  },\n                ),\n              ),\n            ],\n          ),\n          SettingsSection(\n            title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile(\n                title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)),\n                description: Slider(\n                  value: widget.danmakuController.option.area,\n                  min: 0,\n                  max: 1,\n                  divisions: 8,\n                  label:\n                      '${(widget.danmakuController.option.area * 100).round()}%',\n                  onChanged: (value) {\n                    setState(() => widget.danmakuController.updateOption(\n                          widget.danmakuController.option.copyWith(\n                            area: value,\n                          ),\n                        ));\n                    setting.put(SettingBoxKey.danmakuArea, value);\n                  },\n                ),\n              ),\n              SettingsTile(title: Text('持续时间', style: TextStyle(fontFamily: fontFamily)),\n                description: Slider(\n                  value: widget.danmakuController.option.duration.toDouble(),\n                  min: 2,\n                  max: 16,\n                  divisions: 14,\n                  label:\n                      '${widget.danmakuController.option.duration.round()}',\n                  onChanged: (value) {\n                    setState(() => widget.danmakuController.updateOption(\n                          widget.danmakuController.option.copyWith(\n                            duration: value,\n                          ),\n                        ));\n                    setting.put(SettingBoxKey.danmakuDuration, value.round().toDouble());\n                  },\n                ),\n              ),\n              SettingsTile(\n                title: Text('行高', style: TextStyle(fontFamily: fontFamily)),\n                description: Slider(\n                  value: widget.danmakuController.option.lineHeight,\n                  min: 0,\n                  max: 3,\n                  divisions: 30,\n                  label: widget.danmakuController.option.lineHeight.toStringAsFixed(1),\n                  onChanged: (value) {\n                    setState(() => widget.danmakuController.updateOption(\n                          widget.danmakuController.option.copyWith(\n                            lineHeight: double.parse(value.toStringAsFixed(1)),\n                          ),\n                        ));\n                    setting.put(SettingBoxKey.danmakuLineHeight, double.parse(value.toStringAsFixed(1)));\n                  },\n                ),\n              ),\n              SettingsTile.switchTile(\n                onToggle: (value) async {\n                  bool show = value ?? widget.danmakuController.option.hideTop;\n                  setState(() => widget.danmakuController.updateOption(\n                        widget.danmakuController.option.copyWith(\n                          hideTop: !show,\n                        ),\n                      ));\n                  setting.put(SettingBoxKey.danmakuTop, show);\n                },\n                title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)),\n                initialValue: !widget.danmakuController.option.hideTop,\n              ),\n              SettingsTile.switchTile(\n                onToggle: (value) async {\n                  bool show = value ?? widget.danmakuController.option.hideBottom;\n                  setState(() => widget.danmakuController.updateOption(\n                        widget.danmakuController.option.copyWith(\n                          hideBottom: !show,\n                        ),\n                      ));\n                  setting.put(SettingBoxKey.danmakuBottom, show);\n                },\n                title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)),\n                initialValue: !widget.danmakuController.option.hideBottom,\n              ),\n              SettingsTile.switchTile(\n                onToggle: (value) async {\n                  bool show = value ?? widget.danmakuController.option.hideScroll;\n                  setState(() => widget.danmakuController.updateOption(\n                        widget.danmakuController.option.copyWith(\n                          hideScroll: !show,\n                        ),\n                      ));\n                  setting.put(SettingBoxKey.danmakuScroll, show);\n                },\n                title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)),\n                initialValue: !widget.danmakuController.option.hideScroll,\n              ),\n              SettingsTile.switchTile(\n                onToggle: (value) async {\n                  bool followSpeed = value ?? !setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true);\n                  setting.put(SettingBoxKey.danmakuFollowSpeed, followSpeed);\n                  widget.onUpdateDanmakuSpeed?.call();\n                  setState(() {});\n                },\n                title: Text('跟随视频倍速', style: TextStyle(fontFamily: fontFamily)),\n                description: Text('弹幕速度随视频倍速变化', style: TextStyle(fontFamily: fontFamily)),\n                initialValue: setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/danmaku/danmaku_shield_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/pages/my/my_controller.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass DanmakuShieldSettings extends StatefulWidget {\n  const DanmakuShieldSettings({super.key});\n\n  @override\n  State<DanmakuShieldSettings> createState() => _DanmakuShieldSettingsState();\n}\n\nclass _DanmakuShieldSettingsState extends State<DanmakuShieldSettings> {\n  final MyController myController = Modular.get<MyController>();\n  final TextEditingController textEditingController = TextEditingController();\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: SysAppBar(\n        title: const Text(\"弹幕屏蔽\"),\n      ),\n      body: ListView(\n        padding: EdgeInsets.all(12),\n        children: [\n          TextField(\n            controller: textEditingController,\n            decoration: InputDecoration(\n              border: const OutlineInputBorder(),\n              hintText: \"输入关键词或正则表达式\",\n              suffixIcon: TextButton.icon(\n                onPressed: () {\n                  myController.addShieldList(\n                    textEditingController.text.trim(),\n                  );\n                },\n                icon: const Icon(Icons.add),\n                label: const Text(\"添加\"),\n              ),\n            ),\n            onSubmitted: (_) {\n              myController.addShieldList(\n                textEditingController.text.trim(),\n              );\n            },\n          ),\n          SizedBox(height: 12),\n          Text(\n            '以\"/\"开头和结尾将视作正则表达式, 如\"/\\\\d+/\"表示屏蔽所有数字',\n          ),\n          Observer(builder: (context) {\n            return Text(\n              \"已添加${myController.shieldList.length}个关键词\",\n            );\n          }),\n          SizedBox(height: 12),\n          Observer(builder: (context) {\n            return Wrap(\n              runSpacing: 12,\n              spacing: 12,\n              children: myController.shieldList\n                  .map(\n                    (item) => Chip(\n                      shape: RoundedRectangleBorder(\n                          borderRadius: BorderRadius.circular(32)),\n                      backgroundColor:\n                          Theme.of(context).colorScheme.secondaryContainer,\n                      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n                      side: BorderSide.none,\n                      label: Text(\n                        item,\n                        style: TextStyle(\n                          fontSize: 14,\n                        ),\n                      ),\n                      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n                      deleteIcon: Icon(Icons.close, size: 18),\n                      deleteButtonTooltipMessage: '',\n                      onDeleted: () {\n                        myController.removeShieldList(item);\n                      },\n                    ),\n                  )\n                  .toList(),\n            );\n          })\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/decoder_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass DecoderSettings extends StatefulWidget {\n  const DecoderSettings({super.key});\n\n  @override\n  State<DecoderSettings> createState() => _DecoderSettingsState();\n}\n\nclass _DecoderSettingsState extends State<DecoderSettings> {\n  late final Box setting = GStorage.setting;\n  late final ValueNotifier<String> decoder = ValueNotifier<String>(\n    setting.get(SettingBoxKey.hardwareDecoder, defaultValue: 'auto-safe'),\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('硬件解码器'),\n      ),\n      body: SettingsList(\n        maxWidth: 1000,\n        sections: [\n          SettingsSection(\n            title: Text('选择不受支持的解码器将回退到软件解码', style: TextStyle(fontFamily: fontFamily)),\n            tiles: hardwareDecodersList.entries\n                .map((e) => SettingsTile<String>.radioTile(\n                      title: Text(e.key, style: TextStyle(fontFamily: fontFamily)),\n                      description: Text(e.value, style: TextStyle(fontFamily: fontFamily)),\n                      radioValue: e.key,\n                      groupValue: decoder.value,\n                      onChanged: (String? value) {\n                        if (value != null) {\n                          setting.put(SettingBoxKey.hardwareDecoder, value);\n                          setState(() {\n                            decoder.value = value;\n                          });\n                        }\n                      },\n                    ))\n                .toList(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/displaymode_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_displaymode/flutter_displaymode.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass SetDisplayMode extends StatefulWidget {\n  const SetDisplayMode({super.key});\n\n  @override\n  State<SetDisplayMode> createState() => _SetDisplayModeState();\n}\n\nclass _SetDisplayModeState extends State<SetDisplayMode> {\n  List<DisplayMode> modes = <DisplayMode>[];\n  DisplayMode? active;\n  DisplayMode? preferred;\n  Box setting = GStorage.setting;\n\n  final ValueNotifier<int> page = ValueNotifier<int>(0);\n  late final PageController controller = PageController()\n    ..addListener(() {\n      page.value = controller.page!.round();\n    });\n\n  @override\n  void initState() {\n    super.initState();\n    init();\n    SchedulerBinding.instance.addPostFrameCallback((_) {\n      fetchAll();\n    });\n  }\n\n  Future<void> fetchAll() async {\n    preferred = await FlutterDisplayMode.preferred;\n    active = await FlutterDisplayMode.active;\n    await setting.put(SettingBoxKey.displayMode, preferred.toString());\n    setState(() {});\n  }\n\n  Future<void> init() async {\n    try {\n      modes = await FlutterDisplayMode.supported;\n    } on PlatformException catch (_) {}\n    var res = await getDisplayModeType(modes);\n\n    preferred = modes.toList().firstWhere((el) => el == res);\n    FlutterDisplayMode.setPreferredMode(preferred!);\n  }\n\n  Future<DisplayMode> getDisplayModeType(modes) async {\n    var value = setting.get(SettingBoxKey.displayMode);\n    DisplayMode f = DisplayMode.auto;\n    if (value != null) {\n      f = modes.firstWhere((e) => e.toString() == value);\n    }\n    return f;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return Scaffold(\n      appBar: AppBar(title: const Text('屏幕帧率设置')),\n      body: (modes.isEmpty)\n          ? const CircularProgressIndicator()\n          : SettingsList(\n              maxWidth: 1000,\n              sections: [\n                SettingsSection(\n                  title: Text('没有生效? 重启app试试', style: TextStyle(fontFamily: fontFamily)),\n                  tiles: modes\n                      .map((e) => SettingsTile<DisplayMode>.radioTile(\n                            radioValue: e,\n                            groupValue: preferred,\n                            onChanged: (DisplayMode? newMode) async {\n                              await FlutterDisplayMode.setPreferredMode(\n                                  newMode!);\n                              await Future<dynamic>.delayed(\n                                const Duration(milliseconds: 100),\n                              );\n                              await fetchAll();\n                            },\n                            title: e == DisplayMode.auto\n                                ? Text('自动', style: TextStyle(fontFamily: fontFamily))\n                                : Text('$e${e == active ? \"  [系统]\" : \"\"}', style: TextStyle(fontFamily: fontFamily)),\n                          ))\n                      .toList(),\n                ),\n              ],\n            ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/download_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass DownloadSettingsPage extends StatefulWidget {\n  const DownloadSettingsPage({super.key});\n\n  @override\n  State<DownloadSettingsPage> createState() => _DownloadSettingsPageState();\n}\n\nclass _DownloadSettingsPageState extends State<DownloadSettingsPage> {\n  Box setting = GStorage.setting;\n  late int parallelEpisodes;\n  late int parallelSegments;\n  late bool downloadDanmaku;\n\n  @override\n  void initState() {\n    super.initState();\n    parallelEpisodes = setting.get(\n      SettingBoxKey.downloadParallelEpisodes,\n      defaultValue: 2,\n    );\n    parallelSegments = setting.get(\n      SettingBoxKey.downloadParallelSegments,\n      defaultValue: 3,\n    );\n    downloadDanmaku = setting.get(\n      SettingBoxKey.downloadDanmaku,\n      defaultValue: true,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return Scaffold(\n      appBar: const SysAppBar(title: Text('下载设置')),\n      body: SettingsList(\n        maxWidth: 1000,\n        sections: [\n          SettingsSection(\n            title: Text('并发设置', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile(\n                title: Text('同时下载集数', style: TextStyle(fontFamily: fontFamily)),\n                description: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      '同时下载 $parallelEpisodes 集',\n                      style: TextStyle(fontFamily: fontFamily),\n                    ),\n                    Slider(\n                      value: parallelEpisodes.toDouble(),\n                      min: 1,\n                      max: 5,\n                      divisions: 4,\n                      label: '$parallelEpisodes',\n                      onChanged: (value) {\n                        setState(() => parallelEpisodes = value.toInt());\n                        setting.put(\n                          SettingBoxKey.downloadParallelEpisodes,\n                          parallelEpisodes,\n                        );\n                      },\n                    ),\n                  ],\n                ),\n              ),\n              SettingsTile(\n                title: Text('分片并发数', style: TextStyle(fontFamily: fontFamily)),\n                description: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      '每集同时下载 $parallelSegments 个分片',\n                      style: TextStyle(fontFamily: fontFamily),\n                    ),\n                    Slider(\n                      value: parallelSegments.toDouble(),\n                      min: 1,\n                      max: 10,\n                      divisions: 9,\n                      label: '$parallelSegments',\n                      onChanged: (value) {\n                        setState(() => parallelSegments = value.toInt());\n                        setting.put(\n                          SettingBoxKey.downloadParallelSegments,\n                          parallelSegments,\n                        );\n                      },\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n          SettingsSection(\n            title: Text('缓存设置', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile.switchTile(\n                onToggle: (value) {\n                  setState(() => downloadDanmaku = value ?? !downloadDanmaku);\n                  setting.put(SettingBoxKey.downloadDanmaku, downloadDanmaku);\n                },\n                title: Text('缓存弹幕', style: TextStyle(fontFamily: fontFamily)),\n                description: Text(\n                  '下载视频时同时缓存弹幕数据',\n                  style: TextStyle(fontFamily: fontFamily),\n                ),\n                initialValue: downloadDanmaku,\n              ),\n            ],\n          ),\n          SettingsSection(\n            title: Text('说明', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile(\n                title: Text('关于并发设置', style: TextStyle(fontFamily: fontFamily)),\n                description: Text(\n                  '• 集数并发：同时下载多少集视频\\n'\n                  '• 分片并发：每集内同时下载多少个视频片段\\n'\n                  '• 较高的并发可提升速度，但可能被服务器限制\\n'\n                  '• 修改后对新开始的下载生效',\n                  style: TextStyle(fontFamily: fontFamily),\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/interface_settings.dart",
    "content": "import 'package:card_settings_ui/list/settings_list.dart';\nimport 'package:card_settings_ui/section/settings_section.dart';\nimport 'package:card_settings_ui/tile/settings_tile.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\n\nclass InterfaceSettingsPage extends StatefulWidget {\n  const InterfaceSettingsPage({super.key});\n\n  @override\n  State<InterfaceSettingsPage> createState() => _InterfaceSettingsPageState();\n}\n\nclass _InterfaceSettingsPageState extends State<InterfaceSettingsPage> {\n  Box setting = GStorage.setting;\n  late bool showRating;\n  late String defaultPage;\n  final MenuController defaultPageMenuController = MenuController();\n\n  static const Map<String, String> defaultPageMap = {\n    '/tab/popular/': '推荐',\n    '/tab/timeline/': '时间表',\n    '/tab/collect/': '追番',\n    '/tab/my/': '我的',\n  };\n\n  @override\n  void initState() {\n    super.initState();\n    showRating = setting.get(SettingBoxKey.showRating, defaultValue: true);\n    defaultPage = setting.get(SettingBoxKey.defaultStartupPage,\n        defaultValue: '/tab/popular/');\n  }\n\n  void updateDefaultPage(String page) {\n    setting.put(SettingBoxKey.defaultStartupPage, page);\n    setState(() {\n      defaultPage = page;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n\n    return Scaffold(\n      appBar: SysAppBar(\n        title: Text('界面设置'),\n      ),\n      body: SettingsList(\n        sections: [\n          SettingsSection(tiles: [\n            SettingsTile.navigation(\n              onPressed: (_) async {\n                if (defaultPageMenuController.isOpen) {\n                  defaultPageMenuController.close();\n                } else {\n                  defaultPageMenuController.open();\n                }\n              },\n              title: Text('启动界面设置', style: TextStyle(fontFamily: fontFamily)),\n              description: Text('设置应用开启时的默认页面',\n                  style: TextStyle(fontFamily: fontFamily)),\n              value: MenuAnchor(\n                consumeOutsideTap: true,\n                controller: defaultPageMenuController,\n                builder: (_, __, ___) {\n                  return Text(\n                    defaultPageMap[defaultPage] ?? '推荐',\n                    style: TextStyle(fontFamily: fontFamily),\n                  );\n                },\n                menuChildren: [\n                  for (final entry in defaultPageMap.entries)\n                    MenuItemButton(\n                      requestFocusOnHover: false,\n                      onPressed: () => updateDefaultPage(entry.key),\n                      child: Container(\n                        height: 48,\n                        constraints: BoxConstraints(minWidth: 112),\n                        child: Align(\n                          alignment: Alignment.centerLeft,\n                          child: Text(\n                            entry.value,\n                            style: TextStyle(\n                              color: entry.key == defaultPage\n                                  ? Theme.of(context).colorScheme.primary\n                                  : null,\n                              fontFamily: fontFamily,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                ],\n              ),\n            ),\n          ]),\n          SettingsSection(tiles: [\n            SettingsTile.switchTile(\n              onToggle: (value) async {\n                showRating = value ?? !showRating;\n                await setting.put(SettingBoxKey.showRating, showRating);\n                setState(() {});\n              },\n              title: Text('显示评分', style: TextStyle(fontFamily: fontFamily)),\n              description: Text('关闭后将在概览中隐藏评分信息',\n                  style: TextStyle(fontFamily: fontFamily)),\n              initialValue: showRating,\n            ),\n          ]),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/keyboard_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\n\nclass KeyboardSettingsPage extends StatefulWidget {\n  const KeyboardSettingsPage({super.key});\n\n  @override\n  State<KeyboardSettingsPage> createState() => _KeyboardSettingsPageState();\n}\n\nclass _KeyboardSettingsPageState extends State<KeyboardSettingsPage> {\n  Box setting = GStorage.setting;\n\n  String? listeningFunction;\n  int? listeningIndex;\n  late Map<String, List<String>> shortcuts;\n\n  final FocusNode focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();    \n    // 根据默认快捷键生成可用快捷键列表，并读取已设置值\n    shortcuts = {\n      for (var key in defaultShortcuts.keys)\n        key: (setting.get('shortcut_$key', \n                defaultValue: defaultShortcuts[key]?.toList() ?? <String>[]) \n              ?.cast<String>() ?? [])\n    };\n  }\n\n  @override\n  void dispose() {\n    // 清理空的快捷键设置\n    for (final entry in shortcuts.entries) {\n      final func = entry.key;\n      final keys = entry.value;\n      keys.removeWhere((key) => key.isEmpty || key == '...');\n      setting.put('shortcut_$func', keys);\n    }\n    focusNode.dispose();\n    super.dispose();\n  }\n  bool handleShortcutInput(String rawKey) {\n    if (listeningFunction == null || listeningIndex == null) return false;\n\n    final func = listeningFunction!;\n    final index = listeningIndex!;\n\n    // 冲突规避\n    for (final entry in shortcuts.entries) {\n      final otherFunc = entry.key;\n      final otherKeys = entry.value;\n\n      for (int i = 0; i < otherKeys.length; i++) {\n        if (otherFunc == func && i == index) continue;\n        if (otherKeys[i] == rawKey) {\n          final name = shortcutsChineseName[otherFunc] ?? otherFunc;\n          KazumiDialog.showToast(message: \"按键已被【$name】占用，请重新输入\");\n          return true;\n        }\n      }\n    }\n    setState(() {\n      shortcuts[func]![index] = rawKey;\n      listeningFunction = null;\n      listeningIndex = null;\n    });\n    setting.put('shortcut_$func', shortcuts[func]);\n\n    return true;\n  }\n\n  void startListening(String func, int index) {\n    setState(() {\n      listeningFunction = func;\n      listeningIndex = index;\n      shortcuts[func]![index] = '...';\n    });\n\n    Future.delayed(const Duration(milliseconds: 50), () {\n      focusNode.requestFocus();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: SysAppBar(\n        title: Text('快捷键'),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.refresh),\n            tooltip: '恢复默认',\n            onPressed: () {\n              setState(() {\n                for (final func in shortcuts.keys) {\n                  shortcuts[func] = defaultShortcuts[func]?.toList() ?? [];\n                  setting.put('shortcut_$func', shortcuts[func]);\n                }\n              });\n            },\n          ),\n        ],\n      ),\n      body: FocusScope(\n        autofocus: true,\n        child: Focus(\n          focusNode: focusNode,\n          autofocus: true,\n          canRequestFocus: true,\n          skipTraversal: true,\n          descendantsAreFocusable: true,\n          onKeyEvent: (node, event) {\n            if (event is! KeyDownEvent) return KeyEventResult.ignored;\n            if (listeningFunction == null) return KeyEventResult.ignored;\n\n            final rawKey = event.logicalKey.keyLabel.isNotEmpty\n                ? event.logicalKey.keyLabel\n                : event.logicalKey.debugName ?? '';\n\n            final handled = handleShortcutInput(rawKey);\n            return handled ? KeyEventResult.handled : KeyEventResult.ignored;\n          },\n          child: ListView(\n            padding: const EdgeInsets.all(16),\n            children:\n              shortcuts.entries.map((entry) {\n                final func = entry.key;\n                final keys = entry.value;\n                return Card(\n                margin: const EdgeInsets.symmetric(vertical: 8),\n                child: Padding(\n                  padding: const EdgeInsets.all(12),\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Row(\n                        children: [\n                          Text(\n                            shortcutsChineseName[func] ?? func,\n                            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)\n                          ),\n                          Spacer(),\n                          IconButton(\n                            icon: Icon(Icons.add),\n                            onPressed: () {\n                              keys.removeWhere((key) => key.isEmpty || key == '...');\n                              setState(() => keys.add(''));\n                              setting.put('shortcut_$func', keys);\n                              startListening(func, keys.length - 1);\n                            },\n                            padding: EdgeInsets.zero,\n                            constraints: const BoxConstraints(),\n                            focusNode: FocusNode(canRequestFocus: false),\n                          ),\n                        ],\n                      ),\n                      if (keys.isNotEmpty) const SizedBox(height: 8),\n                      Wrap(\n                        spacing: 8,\n                        runSpacing: 8,\n                        children: [\n                          for (int i = 0; i < keys.length; i++)\n                          ActionChip(\n                            label: Text(keyAliases[keys[i]] ?? keys[i],),\n                            avatar: keys.length >=2 ?Icon(Icons.cancel) :Icon(Icons.edit),\n                            onPressed: (keys.length >=2)\n                              ?() {\n                                setState(() {\n                                  keys.removeAt(i);\n                                  listeningIndex = null;\n                                  if (keys.length >1){\n                                    keys.removeWhere((key) => key.isEmpty || key == '...');\n                                  }\n                                  setting.put('shortcut_$func', keys);\n                                });\n                              }\n                              :() => startListening(func, 0),\n                            focusNode: FocusNode(canRequestFocus: false),\n                          ),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n              );\n            }).toList(),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/player_settings.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart' show FilteringTextInputFormatter;\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass PlayerSettingsPage extends StatefulWidget {\n  const PlayerSettingsPage({super.key});\n\n  @override\n  State<PlayerSettingsPage> createState() => _PlayerSettingsPageState();\n}\n\nclass _PlayerSettingsPageState extends State<PlayerSettingsPage> {\n  Box setting = GStorage.setting;\n  late double defaultPlaySpeed;\n  late double defaultShortcutForwardPlaySpeed;\n  late int defaultAspectRatioType;\n  late bool hAenable;\n  late bool androidEnableOpenSLES;\n  late bool lowMemoryMode;\n  late bool playResume;\n  late bool showPlayerError;\n  late bool privateMode;\n  late bool playerDebugMode;\n  late bool playerDisableAnimations;\n  late bool forceAdBlocker;\n  late bool autoPlayNext;\n  late int playerButtonSkipTime;\n  late int playerArrowKeySkipTime;\n  late int playerLogLevel;\n  final MenuController playerAspectRatioMenuController = MenuController();\n  final MenuController playerLogLevelMenuController = MenuController();\n\n  @override\n  void initState() {\n    super.initState();\n    defaultPlaySpeed =\n        setting.get(SettingBoxKey.defaultPlaySpeed, defaultValue: 1.0);\n    defaultShortcutForwardPlaySpeed = \n        setting.get(SettingBoxKey.defaultShortcutForwardPlaySpeed, defaultValue: 2.0);\n    defaultAspectRatioType =\n        setting.get(SettingBoxKey.defaultAspectRatioType, defaultValue: 1);\n    hAenable = setting.get(SettingBoxKey.hAenable, defaultValue: true);\n    androidEnableOpenSLES =\n        setting.get(SettingBoxKey.androidEnableOpenSLES, defaultValue: true);\n    lowMemoryMode =\n        setting.get(SettingBoxKey.lowMemoryMode, defaultValue: false);\n    playResume = setting.get(SettingBoxKey.playResume, defaultValue: true);\n    privateMode = setting.get(SettingBoxKey.privateMode, defaultValue: false);\n    showPlayerError =\n        setting.get(SettingBoxKey.showPlayerError, defaultValue: true);\n    playerDebugMode =\n        setting.get(SettingBoxKey.playerDebugMode, defaultValue: false);\n    autoPlayNext = setting.get(SettingBoxKey.autoPlayNext, defaultValue: true);\n    playerDisableAnimations =\n        setting.get(SettingBoxKey.playerDisableAnimations, defaultValue: false);\n    forceAdBlocker =\n        setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false);\n    playerLogLevel = setting.get(SettingBoxKey.playerLogLevel, defaultValue: 2);\n\n    playerButtonSkipTime =\n        setting.get(SettingBoxKey.buttonSkipTime, defaultValue: 80);\n    playerArrowKeySkipTime =\n        setting.get(SettingBoxKey.arrowKeySkipTime, defaultValue: 10);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  void updateDefaultPlaySpeed(double speed) {\n    setting.put(SettingBoxKey.defaultPlaySpeed, speed);\n    setState(() {\n      defaultPlaySpeed = speed;\n    });\n  }\n\n  void updateDefaultShortcutForwardPlaySpeed(double speed) {\n    setting.put(SettingBoxKey.defaultShortcutForwardPlaySpeed, speed);\n    setState(() {\n      defaultShortcutForwardPlaySpeed = speed;\n    });\n  }\n\n  void updatePlayerLogLevel(int level) {\n    setting.put(SettingBoxKey.playerLogLevel, level);\n    setState(() {\n      playerLogLevel = level;\n    });\n  }\n\n  void updateDefaultAspectRatioType(int type) {\n    setting.put(SettingBoxKey.defaultAspectRatioType, type);\n    setState(() {\n      defaultAspectRatioType = type;\n    });\n  }\n\n  Future<void> updateButtonSkipTime() async {\n    final int? newButtonSkipTime = await _showSkipTimeChangeDialog(\n        title: '顶部按钮快进时长', initialValue: playerButtonSkipTime.toString());\n    print('新设置的顶部按钮快进时长: $newButtonSkipTime');\n\n    if (newButtonSkipTime != null &&\n        newButtonSkipTime != playerButtonSkipTime) {\n      setting.put(SettingBoxKey.buttonSkipTime, newButtonSkipTime);\n      setState(() {\n        playerButtonSkipTime = newButtonSkipTime;\n      });\n    }\n  }\n\n  Future<int?> _showSkipTimeChangeDialog(\n      {required String title, required String initialValue}) async {\n    return KazumiDialog.show<int>(builder: (context) {\n      String input = \"\";\n      return AlertDialog(\n        title: Text(title),\n        content: StatefulBuilder(\n            builder: (BuildContext context, StateSetter setState) {\n          return TextField(\n            inputFormatters: [\n              FilteringTextInputFormatter.digitsOnly, // 只允许输入数字\n            ],\n            decoration: InputDecoration(\n              floatingLabelBehavior:\n                  FloatingLabelBehavior.never, // 控制label的显示方式\n              labelText: initialValue,\n            ),\n            onChanged: (value) {\n              input = value;\n            },\n          );\n        }),\n        actions: <Widget>[\n          TextButton(\n            onPressed: () => KazumiDialog.dismiss(),\n            child: Text(\n              '取消',\n              style: TextStyle(color: Theme.of(context).colorScheme.outline),\n            ),\n          ),\n          TextButton(\n            onPressed: () async {\n              final int? newValue = int.tryParse(input);\n\n              if (newValue == null) {\n                KazumiDialog.showToast(message: '请输入数字');\n                return;\n              }\n\n              if (newValue <= 0) {\n                KazumiDialog.showToast(message: '请输入大于0的数字');\n                return;\n              }\n              // 以新设置的值弹出\n              KazumiDialog.dismiss(popWith: newValue);\n            },\n            child: const Text('确定'),\n          ),\n        ],\n      );\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('播放设置')),\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    hAenable = value ?? !hAenable;\n                    await setting.put(SettingBoxKey.hAenable, hAenable);\n                    setState(() {});\n                  },\n                  title: Text('硬件解码', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: hAenable,\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    await Modular.to.pushNamed('/settings/player/decoder');\n                  },\n                  title: Text('硬件解码器', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('仅在硬件解码启用时生效', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                if (Platform.isAndroid) ...[\n                  SettingsTile.navigation(\n                    onPressed: (_) async {\n                      await Modular.to.pushNamed('/settings/player/renderer');\n                    },\n                    title: Text('视频渲染器', style: TextStyle(fontFamily: fontFamily)),\n                    description: Text('选择视频输出方式', style: TextStyle(fontFamily: fontFamily)),\n                  ),\n                ],\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    lowMemoryMode = value ?? !lowMemoryMode;\n                    await setting.put(\n                        SettingBoxKey.lowMemoryMode, lowMemoryMode);\n                    setState(() {});\n                  },\n                  title: Text('低内存模式', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('禁用高级缓存以减少内存占用', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: lowMemoryMode,\n                ),\n                if (Platform.isAndroid) ...[\n                  SettingsTile.switchTile(\n                    onToggle: (value) async {\n                      androidEnableOpenSLES = value ?? !androidEnableOpenSLES;\n                      await setting.put(SettingBoxKey.androidEnableOpenSLES,\n                          androidEnableOpenSLES);\n                      setState(() {});\n                    },\n                    title: Text('低延迟音频', style: TextStyle(fontFamily: fontFamily)),\n                    description: Text('启用OpenSLES音频输出以降低延时', style: TextStyle(fontFamily: fontFamily)),\n                    initialValue: androidEnableOpenSLES,\n                  ),\n                ],\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    Modular.to.pushNamed('/settings/player/super');\n                  },\n                  title: Text('超分辨率', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    playResume = value ?? !playResume;\n                    await setting.put(SettingBoxKey.playResume, playResume);\n                    setState(() {});\n                  },\n                  title: Text('自动跳转', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('跳转到上次播放位置', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: playResume,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    autoPlayNext = value ?? !autoPlayNext;\n                    await setting.put(SettingBoxKey.autoPlayNext, autoPlayNext);\n                    setState(() {});\n                  },\n                  title: Text('自动连播', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('当前视频播放完毕后自动播放下一集', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: autoPlayNext,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    forceAdBlocker = value ?? !forceAdBlocker;\n                    await setting.put(SettingBoxKey.forceAdBlocker, forceAdBlocker);\n                    setState(() {});\n                  },\n                  title: Text('广告过滤', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('强制启用HLS广告过滤，忽略规则设置', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: forceAdBlocker,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    playerDisableAnimations = value ?? !playerDisableAnimations;\n                    await setting.put(SettingBoxKey.playerDisableAnimations,\n                        playerDisableAnimations);\n                    setState(() {});\n                  },\n                  title: Text('禁用动画', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('禁用播放器内的过渡动画', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: playerDisableAnimations,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    privateMode = value ?? !privateMode;\n                    await setting.put(SettingBoxKey.privateMode, privateMode);\n                    setState(() {});\n                  },\n                  title: Text('隐身模式', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('不保留观看记录', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: privateMode,\n                ),\n              ],\n            ),\n            SettingsSection(\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    showPlayerError = value ?? !showPlayerError;\n                    await setting.put(\n                        SettingBoxKey.showPlayerError, showPlayerError);\n                    setState(() {});\n                  },\n                  title: Text('错误提示', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('显示播放器内部错误提示', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: showPlayerError,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    playerDebugMode = value ?? !playerDebugMode;\n                    await setting.put(\n                        SettingBoxKey.playerDebugMode, playerDebugMode);\n                    setState(() {});\n                  },\n                  title: Text('调试模式', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('记录播放器内部日志', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: playerDebugMode,\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    if (playerLogLevelMenuController.isOpen) {\n                      playerLogLevelMenuController.close();\n                    } else {\n                      playerLogLevelMenuController.open();\n                    }\n                  },\n                  title: Text('日志等级', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('播放器内部日志等级', style: TextStyle(fontFamily: fontFamily)),\n                  value: MenuAnchor(\n                    consumeOutsideTap: true,\n                    controller: playerLogLevelMenuController,\n                    builder: (_, __, ___) {\n                      return Text(\n                        playerLogLevelMap[playerLogLevel] ?? '???',\n                      );\n                    },\n                    menuChildren: [\n                      for (final entry in playerLogLevelMap.entries)\n                        MenuItemButton(\n                          requestFocusOnHover: false,\n                          onPressed: () => updatePlayerLogLevel(entry.key),\n                          child: Container(\n                            height: 48,\n                            constraints: BoxConstraints(minWidth: 112),\n                            child: Align(\n                              alignment: Alignment.centerLeft,\n                              child: Text(\n                                entry.value,\n                                style: TextStyle(\n                                  color: entry.key == playerLogLevel\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null,\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n            SettingsSection(\n              tiles: [\n                SettingsTile(\n                  title: Text('默认倍速', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultPlaySpeed,\n                    min: 0.25,\n                    max: 3,\n                    divisions: 11,\n                    label: '${defaultPlaySpeed}x',\n                    onChanged: (value) {\n                      updateDefaultPlaySpeed(\n                          double.parse(value.toStringAsFixed(2)));\n                    },\n                  ),\n                ),\n                SettingsTile(\n                  title: Text('默认方向键倍速', style: TextStyle(fontFamily: fontFamily)),\n                  description: Slider(\n                    value: defaultShortcutForwardPlaySpeed,\n                    min: 1.25,\n                    max: 3,\n                    divisions: 7,\n                    label: '${defaultShortcutForwardPlaySpeed}x',\n                    onChanged: (value) {\n                      updateDefaultShortcutForwardPlaySpeed(\n                          double.parse(value.toStringAsFixed(2)));\n                    },\n                  ),\n                ),\n                SettingsTile.navigation(\n                  description: Slider(\n                    value: playerArrowKeySkipTime.toDouble(),\n                    min: 0,\n                    max: 15,\n                    divisions: 15,\n                    label: '$playerArrowKeySkipTime秒',\n                    onChanged: (value) {\n                      final newArrowKeySkipTime = value.toInt();\n                      print('新设置的方向键快进/快退时长: $newArrowKeySkipTime');\n\n                      if (value != playerArrowKeySkipTime) {\n                        setting.put(SettingBoxKey.arrowKeySkipTime,\n                            newArrowKeySkipTime);\n                        setState(() {\n                          playerArrowKeySkipTime = newArrowKeySkipTime;\n                        });\n                      }\n                    },\n                  ),\n                  title: Text('左右方向键的快进/快退秒数', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    await updateButtonSkipTime();\n                  },\n                  title: Text('跳过时长', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('顶栏跳过按钮的秒数', style: TextStyle(fontFamily: fontFamily)),\n                  value: Text('$playerButtonSkipTime 秒', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    if (playerAspectRatioMenuController.isOpen) {\n                      playerAspectRatioMenuController.close();\n                    } else {\n                      playerAspectRatioMenuController.open();\n                    }\n                  },\n                  title: Text('默认视频比例', style: TextStyle(fontFamily: fontFamily)),\n                  value: MenuAnchor(\n                    consumeOutsideTap: true,\n                    controller: playerAspectRatioMenuController,\n                    builder: (_, __, ___) {\n                      return Text(\n                        aspectRatioTypeMap[defaultAspectRatioType] ?? '自动',\n                        style: TextStyle(fontFamily: fontFamily),\n                      );\n                    },\n                    menuChildren: [\n                      for (final entry in aspectRatioTypeMap.entries)\n                        MenuItemButton(\n                          requestFocusOnHover: false,\n                          onPressed: () =>\n                              updateDefaultAspectRatioType(entry.key),\n                          child: Container(\n                            height: 48,\n                            constraints: BoxConstraints(minWidth: 112),\n                            child: Align(\n                              alignment: Alignment.centerLeft,\n                              child: Text(\n                                entry.value,\n                                style: TextStyle(\n                                  color: entry.key == defaultAspectRatioType\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null,\n                                  fontFamily: fontFamily,\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/proxy/proxy_editor_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:dio/dio.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/proxy_manager.dart';\nimport 'package:kazumi/request/request.dart';\n\nclass ProxyEditorPage extends StatefulWidget {\n  const ProxyEditorPage({super.key});\n\n  @override\n  State<ProxyEditorPage> createState() => _ProxyEditorPageState();\n}\n\nclass _ProxyEditorPageState extends State<ProxyEditorPage> {\n  Box setting = GStorage.setting;\n  final _formKey = GlobalKey<FormState>();\n  final TextEditingController urlController = TextEditingController();\n  final TextEditingController testUrlController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    urlController.text =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    testUrlController.text =\n        setting.get(SettingBoxKey.proxyTestUrl, defaultValue: 'https://www.google.com');\n  }\n\n  @override\n  void dispose() {\n    urlController.dispose();\n    testUrlController.dispose();\n    super.dispose();\n  }\n\n  Future<void> saveAndTest() async {\n    if (!_formKey.currentState!.validate()) {\n      return;\n    }\n\n    final url = urlController.text.trim();\n    if (url.isEmpty) {\n      KazumiDialog.showToast(message: '请输入代理地址');\n      return;\n    }\n\n    final testUrl = testUrlController.text.trim().isEmpty\n        ? 'https://www.google.com'\n        : testUrlController.text.trim();\n\n    await setting.put(SettingBoxKey.proxyUrl, url);\n    await setting.put(SettingBoxKey.proxyTestUrl, testUrl);\n    // 重置配置状态，等待测试结果\n    await setting.put(SettingBoxKey.proxyConfigured, false);\n\n    // 临时启用代理进行测试\n    await setting.put(SettingBoxKey.proxyEnable, true);\n    ProxyManager.applyProxy();\n\n    try {\n      await Request().get(\n        testUrl,\n        options: Options(\n          sendTimeout: const Duration(seconds: 10),\n          receiveTimeout: const Duration(seconds: 10),\n          validateStatus: (status) => true,\n        ),\n        shouldRethrow: true,\n      ).timeout(const Duration(seconds: 15));\n      await setting.put(SettingBoxKey.proxyConfigured, true);\n      KazumiDialog.showToast(message: '测试成功');\n    } catch (e) {\n      await setting.put(SettingBoxKey.proxyEnable, false);\n      ProxyManager.clearProxy();\n      KazumiDialog.showToast(message: '代理连接失败');\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: const SysAppBar(title: Text('代理配置')),\n      body: SingleChildScrollView(\n        padding: const EdgeInsets.all(16.0),\n        child: Center(\n          child: SizedBox(\n            width: (MediaQuery.of(context).size.width > 800) ? 800 : null,\n            child: Form(\n              key: _formKey,\n              child: Column(\n                children: [\n                  TextFormField(\n                    controller: urlController,\n                    decoration: const InputDecoration(\n                      labelText: '代理地址',\n                      hintText: 'http://127.0.0.1:7890',\n                      border: OutlineInputBorder(),\n                    ),\n                    validator: (value) {\n                      if (value == null || value.isEmpty) {\n                        return '请输入代理地址';\n                      }\n                      if (!ProxyUtils.isValidProxyUrl(value)) {\n                        return '格式错误，请使用 http://host:port 格式';\n                      }\n                      return null;\n                    },\n                  ),\n                  const SizedBox(height: 16),\n                  TextFormField(\n                    controller: testUrlController,\n                    decoration: const InputDecoration(\n                      labelText: '测试地址',\n                      hintText: 'https://www.google.com',\n                      border: OutlineInputBorder(),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton.extended(\n        onPressed: saveAndTest,\n        icon: const Icon(Icons.save),\n        label: const Text('保存并测试'),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/proxy/proxy_module.dart",
    "content": "import 'package:kazumi/pages/settings/proxy/proxy_settings_page.dart';\nimport 'package:kazumi/pages/settings/proxy/proxy_editor_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass ProxyModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const ProxySettingsPage());\n    r.child(\"/editor\", child: (_) => const ProxyEditorPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/proxy/proxy_settings_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_manager.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass ProxySettingsPage extends StatefulWidget {\n  const ProxySettingsPage({super.key});\n\n  @override\n  State<ProxySettingsPage> createState() => _ProxySettingsPageState();\n}\n\nclass _ProxySettingsPageState extends State<ProxySettingsPage> {\n  Box setting = GStorage.setting;\n  late bool proxyEnable;\n\n  @override\n  void initState() {\n    super.initState();\n    proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  Future<void> updateProxyEnable(bool value) async {\n    if (value) {\n      final proxyConfigured =\n          setting.get(SettingBoxKey.proxyConfigured, defaultValue: false);\n      if (!proxyConfigured) {\n        KazumiDialog.showToast(message: '请先在代理配置中完成测试');\n        return;\n      }\n      await setting.put(SettingBoxKey.proxyEnable, true);\n      ProxyManager.applyProxy();\n    } else {\n      await setting.put(SettingBoxKey.proxyEnable, false);\n      ProxyManager.clearProxy();\n    }\n    setState(() {\n      proxyEnable = value;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('代理设置')),\n        body: SettingsList(\n          maxWidth: 800,\n          sections: [\n            SettingsSection(\n              title: Text('代理', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    await updateProxyEnable(value ?? !proxyEnable);\n                  },\n                  title:\n                      Text('启用代理', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('启用后网络请求将通过代理服务器',\n                      style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: proxyEnable,\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    await Modular.to.pushNamed('/settings/proxy/editor');\n                    setState(() {\n                      proxyEnable = setting.get(SettingBoxKey.proxyEnable,\n                          defaultValue: false);\n                    });\n                  },\n                  title:\n                      Text('代理配置', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('配置代理服务器地址和认证信息',\n                      style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/renderer_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass RendererSettings extends StatefulWidget {\n  const RendererSettings({super.key});\n\n  @override\n  State<RendererSettings> createState() => _RendererSettingsState();\n}\n\nclass _RendererSettingsState extends State<RendererSettings> {\n  late final Box setting = GStorage.setting;\n  late final ValueNotifier<String> renderer = ValueNotifier<String>(\n    setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto'),\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('视频渲染器'),\n      ),\n      body: SettingsList(\n        maxWidth: 1000,\n        sections: [\n          SettingsSection(\n            title: Text('选择合适的渲染器以获得最佳播放体验',\n                style: TextStyle(fontFamily: fontFamily)),\n            tiles: androidVideoRenderersList.entries\n                .map((e) => SettingsTile<String>.radioTile(\n                      title:\n                          Text(e.key, style: TextStyle(fontFamily: fontFamily)),\n                      description: Text(e.value,\n                          style: TextStyle(fontFamily: fontFamily)),\n                      radioValue: e.key,\n                      groupValue: renderer.value,\n                      onChanged: (String? value) {\n                        if (value != null) {\n                          setting.put(\n                              SettingBoxKey.androidVideoRenderer, value);\n                          setState(() {\n                            renderer.value = value;\n                          });\n                        }\n                      },\n                    ))\n                .toList(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/settings_module.dart",
    "content": "import 'package:kazumi/pages/settings/danmaku/danmaku_module.dart';\nimport 'package:kazumi/pages/about/about_module.dart';\nimport 'package:kazumi/pages/plugin_editor/plugin_module.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/history/history_module.dart';\nimport 'package:kazumi/pages/settings/interface_settings.dart';\nimport 'package:kazumi/pages/settings/theme_settings_page.dart';\nimport 'package:kazumi/pages/settings/player_settings.dart';\nimport 'package:kazumi/pages/settings/displaymode_settings.dart';\nimport 'package:kazumi/pages/settings/decoder_settings.dart';\nimport 'package:kazumi/pages/settings/renderer_settings.dart';\nimport 'package:kazumi/pages/settings/super_resolution_settings.dart';\nimport 'package:kazumi/pages/settings/proxy/proxy_module.dart';\nimport 'package:kazumi/pages/webdav_editor/webdav_module.dart';\nimport 'package:kazumi/pages/settings/keyboard_settings.dart';\nimport 'package:kazumi/pages/settings/download_settings.dart';\nimport 'package:kazumi/pages/download/download_page_module.dart';\n\nclass SettingsModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/theme\", child: (_) => const ThemeSettingsPage());\n    r.child(\n      \"/theme/display\",\n      child: (_) => const SetDisplayMode(),\n    );\n    r.child(\"/keyboard\", child: (_) => const KeyboardSettingsPage());\n    r.child(\"/player\", child: (_) => const PlayerSettingsPage());\n    r.child(\"/player/decoder\", child: (_) => const DecoderSettings());\n    r.child(\"/player/renderer\", child: (_) => const RendererSettings());\n    r.child(\"/interface\", child: (_) => const InterfaceSettingsPage());\n    r.module(\"/proxy\", module: ProxyModule());\n    r.child(\"/player/super\", child: (_) => const SuperResolutionSettings());\n    r.module(\"/webdav\", module: WebDavModule());\n    r.module(\"/about\", module: AboutModule());\n    r.module(\"/plugin\", module: PluginModule());\n    r.module(\"/history\", module: HistoryModule());\n    r.module(\"/danmaku\", module: DanmakuModule());\n    r.module(\"/download\", module: DownloadModule());\n    r.child(\"/download-settings\", child: (_) => const DownloadSettingsPage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/super_resolution_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass SuperResolutionSettings extends StatefulWidget {\n  const SuperResolutionSettings({super.key});\n\n  @override\n  State<SuperResolutionSettings> createState() =>\n      _SuperResolutionSettingsState();\n}\n\nclass _SuperResolutionSettingsState extends State<SuperResolutionSettings> {\n  late final Box setting = GStorage.setting;\n  late bool promptOnEnable;\n  late final ValueNotifier<String> superResolutionType = ValueNotifier<String>(\n    setting\n        .get(SettingBoxKey.defaultSuperResolutionType, defaultValue: 1)\n        .toString(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    promptOnEnable =\n        setting.get(SettingBoxKey.superResolutionWarn, defaultValue: false);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('超分辨率'),\n      ),\n      body: SettingsList(\n        maxWidth: 1000,\n        sections: [\n          SettingsSection(\n              title: Text(\n                  '超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换视频渲染器为 gpu', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile<String>.radioTile(\n                  title: Text(\"OFF\", style: TextStyle(fontFamily: fontFamily)),\n                  description: Text(\"默认禁用超分辨率\", style: TextStyle(fontFamily: fontFamily)),\n                  radioValue: \"1\",\n                  groupValue: superResolutionType.value,\n                  onChanged: (String? value) {\n                    if (value != null) {\n                      setting.put(SettingBoxKey.defaultSuperResolutionType,\n                          int.tryParse(value) ?? 1);\n                      setState(() {\n                        superResolutionType.value = value;\n                      });\n                    }\n                  },\n                ),\n                SettingsTile<String>.radioTile(\n                  title: Text(\"Efficiency\", style: TextStyle(fontFamily: fontFamily)),\n                  description: Text(\"默认启用基于Anime4K的超分辨率 (效率优先)\", style: TextStyle(fontFamily: fontFamily)),\n                  radioValue: \"2\",\n                  groupValue: superResolutionType.value,\n                  onChanged: (String? value) {\n                    if (value != null) {\n                      setting.put(SettingBoxKey.defaultSuperResolutionType,\n                          int.tryParse(value) ?? 1);\n                      setState(() {\n                        superResolutionType.value = value;\n                      });\n                    }\n                  },\n                ),\n                SettingsTile<String>.radioTile(\n                  title: Text(\"Quality\", style: TextStyle(fontFamily: fontFamily)),\n                  description: Text(\"默认启用基于Anime4K的超分辨率 (质量优先)\", style: TextStyle(fontFamily: fontFamily)),\n                  radioValue: \"3\",\n                  groupValue: superResolutionType.value,\n                  onChanged: (String? value) {\n                    if (value != null) {\n                      setting.put(SettingBoxKey.defaultSuperResolutionType,\n                          int.tryParse(value) ?? 1);\n                      setState(() {\n                        superResolutionType.value = value;\n                      });\n                    }\n                  },\n                )\n              ]),\n          SettingsSection(\n            title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)),\n            tiles: [\n              SettingsTile.switchTile(\n                title: Text('关闭提示', style: TextStyle(fontFamily: fontFamily)),\n                description: Text('关闭每次启用超分辨率时的提示', style: TextStyle(fontFamily: fontFamily)),\n                initialValue: promptOnEnable,\n                onToggle: (value) async {\n                  promptOnEnable = value ?? !promptOnEnable;\n                  await setting.put(\n                      SettingBoxKey.superResolutionWarn, promptOnEnable);\n                  if (mounted) setState(() {});\n                },\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings/theme_settings_page.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/card/palette_card.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/settings/theme_provider.dart';\nimport 'package:kazumi/pages/popular/popular_controller.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/bean/settings/color_type.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\nimport 'package:provider/provider.dart';\n\nclass ThemeSettingsPage extends StatefulWidget {\n  const ThemeSettingsPage({super.key});\n\n  @override\n  State<ThemeSettingsPage> createState() => _ThemeSettingsPageState();\n}\n\nclass _ThemeSettingsPageState extends State<ThemeSettingsPage> {\n  Box setting = GStorage.setting;\n  late dynamic defaultDanmakuArea;\n  late dynamic defaultThemeMode;\n  late dynamic defaultThemeColor;\n  late bool oledEnhance;\n  late bool useDynamicColor;\n  late bool showWindowButton;\n  late bool useSystemFont;\n  final PopularController popularController = Modular.get<PopularController>();\n  late final ThemeProvider themeProvider;\n  final MenuController menuController = MenuController();\n\n  @override\n  void initState() {\n    super.initState();\n    defaultThemeMode =\n        setting.get(SettingBoxKey.themeMode, defaultValue: 'system');\n    defaultThemeColor =\n        setting.get(SettingBoxKey.themeColor, defaultValue: 'default');\n    oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false);\n    useDynamicColor =\n        setting.get(SettingBoxKey.useDynamicColor, defaultValue: false);\n    showWindowButton =\n        setting.get(SettingBoxKey.showWindowButton, defaultValue: false);\n    useSystemFont =\n        setting.get(SettingBoxKey.useSystemFont, defaultValue: false);\n    themeProvider = Provider.of<ThemeProvider>(context, listen: false);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  void setTheme(Color? color) {\n    var defaultDarkTheme = ThemeData(\n        useMaterial3: true,\n        fontFamily: themeProvider.currentFontFamily,\n        brightness: Brightness.dark,\n        colorSchemeSeed: color,\n        progressIndicatorTheme: progressIndicatorTheme2024,\n        sliderTheme: sliderTheme2024,\n        pageTransitionsTheme: pageTransitionsTheme2024);\n    var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme);\n    themeProvider.setTheme(\n      ThemeData(\n          useMaterial3: true,\n          fontFamily: themeProvider.currentFontFamily,\n          brightness: Brightness.light,\n          colorSchemeSeed: color,\n          progressIndicatorTheme: progressIndicatorTheme2024,\n          sliderTheme: sliderTheme2024,\n          pageTransitionsTheme: pageTransitionsTheme2024),\n      oledEnhance ? oledDarkTheme : defaultDarkTheme,\n    );\n    defaultThemeColor = color?.value.toRadixString(16) ?? 'default';\n    setting.put(SettingBoxKey.themeColor, defaultThemeColor);\n  }\n\n  void resetTheme() {\n    var defaultDarkTheme = ThemeData(\n        useMaterial3: true,\n        fontFamily: themeProvider.currentFontFamily,\n        brightness: Brightness.dark,\n        colorSchemeSeed: Colors.green,\n        progressIndicatorTheme: progressIndicatorTheme2024,\n        sliderTheme: sliderTheme2024,\n        pageTransitionsTheme: pageTransitionsTheme2024);\n    var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme);\n    themeProvider.setTheme(\n      ThemeData(\n          useMaterial3: true,\n          fontFamily: themeProvider.currentFontFamily,\n          brightness: Brightness.light,\n          colorSchemeSeed: Colors.green,\n          progressIndicatorTheme: progressIndicatorTheme2024,\n          sliderTheme: sliderTheme2024,\n          pageTransitionsTheme: pageTransitionsTheme2024),\n      oledEnhance ? oledDarkTheme : defaultDarkTheme,\n    );\n    defaultThemeColor = 'default';\n    setting.put(SettingBoxKey.themeColor, 'default');\n  }\n\n  void updateTheme(String theme) async {\n    if (theme == 'dark') {\n      themeProvider.setThemeMode(ThemeMode.dark);\n    }\n    if (theme == 'light') {\n      themeProvider.setThemeMode(ThemeMode.light);\n    }\n    if (theme == 'system') {\n      themeProvider.setThemeMode(ThemeMode.system);\n    }\n    await setting.put(SettingBoxKey.themeMode, theme);\n    setState(() {\n      defaultThemeMode = theme;\n    });\n  }\n\n  void updateOledEnhance() {\n    dynamic color;\n    oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false);\n    if (defaultThemeColor == 'default') {\n      color = Colors.green;\n    } else {\n      color = Color(int.parse(defaultThemeColor, radix: 16));\n    }\n    setTheme(color);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('外观设置')),\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              title: Text('外观', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.navigation(\n                  onPressed: (_) {\n                    if (menuController.isOpen) {\n                      menuController.close();\n                    } else {\n                      menuController.open();\n                    }\n                  },\n                  title: Text('深色模式', style: TextStyle(fontFamily: fontFamily)),\n                  value: MenuAnchor(\n                    consumeOutsideTap: true,\n                    controller: menuController,\n                    builder: (_, __, ___) {\n                      return Text(\n                        defaultThemeMode == 'light'\n                            ? '浅色'\n                            : (defaultThemeMode == 'dark' ? '深色' : '跟随系统'),\n                        style: TextStyle(fontFamily: fontFamily),\n                      );\n                    },\n                    menuChildren: [\n                      MenuItemButton(\n                        requestFocusOnHover: false,\n                        onPressed: () => updateTheme('system'),\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Row(\n                              children: [\n                                Icon(\n                                  Icons.brightness_auto_rounded,\n                                  color: defaultThemeMode == 'system'\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null,\n                                ),\n                                SizedBox(width: 8),\n                                Text(\n                                  '跟随系统',\n                                  style: TextStyle(\n                                    color: defaultThemeMode == 'system'\n                                        ? Theme.of(context).colorScheme.primary\n                                        : null,\n                                    fontFamily: fontFamily,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      ),\n                      MenuItemButton(\n                        requestFocusOnHover: false,\n                        onPressed: () => updateTheme('light'),\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Row(\n                              children: [\n                                Icon(\n                                  Icons.light_mode_rounded,\n                                  color: defaultThemeMode == 'light'\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null,\n                                ),\n                                SizedBox(width: 8),\n                                Text(\n                                  '浅色',\n                                  style: TextStyle(\n                                      color: defaultThemeMode == 'light'\n                                          ? Theme.of(context)\n                                              .colorScheme\n                                              .primary\n                                          : null,\n                                      fontFamily: fontFamily),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      ),\n                      MenuItemButton(\n                        requestFocusOnHover: false,\n                        onPressed: () => updateTheme('dark'),\n                        child: Container(\n                          height: 48,\n                          constraints: BoxConstraints(minWidth: 112),\n                          child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Row(\n                              children: [\n                                Icon(\n                                  Icons.dark_mode_rounded,\n                                  color: defaultThemeMode == 'dark'\n                                      ? Theme.of(context).colorScheme.primary\n                                      : null,\n                                ),\n                                SizedBox(width: 8),\n                                Text(\n                                  '深色',\n                                  style: TextStyle(\n                                    color: defaultThemeMode == 'dark'\n                                        ? Theme.of(context).colorScheme.primary\n                                        : null,\n                                    fontFamily: fontFamily,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                SettingsTile.navigation(\n                  enabled: !useDynamicColor,\n                  onPressed: (_) async {\n                    KazumiDialog.show(builder: (context) {\n                      return AlertDialog(\n                        title: Text('配色方案',\n                            style: TextStyle(fontFamily: fontFamily)),\n                        content: StatefulBuilder(builder:\n                            (BuildContext context, StateSetter setState) {\n                          final List<Map<String, dynamic>> colorThemes =\n                              colorThemeTypes;\n                          return Wrap(\n                            alignment: WrapAlignment.center,\n                            spacing: 8,\n                            runSpacing: Utils.isDesktop() ? 8 : 0,\n                            children: [\n                              ...colorThemes.map(\n                                (e) {\n                                  final index = colorThemes.indexOf(e);\n                                  return GestureDetector(\n                                    onTap: () {\n                                      index == 0\n                                          ? resetTheme()\n                                          : setTheme(e['color']);\n                                      KazumiDialog.dismiss();\n                                    },\n                                    child: Column(\n                                      children: [\n                                        PaletteCard(\n                                          color: e['color'],\n                                          selected: (e['color']\n                                                      .value\n                                                      .toRadixString(16) ==\n                                                  defaultThemeColor ||\n                                              (defaultThemeColor == 'default' &&\n                                                  index == 0)),\n                                        ),\n                                        Text(e['label']),\n                                      ],\n                                    ),\n                                  );\n                                },\n                              )\n                            ],\n                          );\n                        }),\n                      );\n                    });\n                  },\n                  title: Text('配色方案', style: TextStyle(fontFamily: fontFamily)),\n                ),\n                SettingsTile.switchTile(\n                  enabled: !Platform.isIOS,\n                  onToggle: (value) async {\n                    useDynamicColor = value ?? !useDynamicColor;\n                    await setting.put(\n                        SettingBoxKey.useDynamicColor, useDynamicColor);\n                    themeProvider.setDynamic(useDynamicColor);\n                    setState(() {});\n                  },\n                  title: Text('动态配色', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: useDynamicColor,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    useSystemFont = value ?? !useSystemFont;\n                    await setting.put(\n                        SettingBoxKey.useSystemFont, useSystemFont);\n                    themeProvider.setFontFamily(useSystemFont);\n                    dynamic color;\n                    if (defaultThemeColor == 'default') {\n                      color = Colors.green;\n                    } else {\n                      color = Color(int.parse(defaultThemeColor, radix: 16));\n                    }\n                    setTheme(color);\n                    setState(() {});\n                  },\n                  title:\n                      Text('使用系统字体', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('关闭后使用 MI Sans 字体',\n                      style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: useSystemFont,\n                ),\n              ],\n              bottomInfo: Text('动态配色仅支持安卓12及以上和桌面平台',\n                  style: TextStyle(fontFamily: fontFamily)),\n            ),\n            SettingsSection(\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    oledEnhance = value ?? !oledEnhance;\n                    await setting.put(SettingBoxKey.oledEnhance, oledEnhance);\n                    updateOledEnhance();\n                    setState(() {});\n                  },\n                  title:\n                      Text('OLED优化', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('深色模式下使用纯黑背景',\n                      style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: oledEnhance,\n                ),\n              ],\n            ),\n            if (Utils.isDesktop())\n              SettingsSection(\n                tiles: [\n                  SettingsTile.switchTile(\n                    onToggle: (value) async {\n                      showWindowButton = value ?? !showWindowButton;\n                      await setting.put(\n                          SettingBoxKey.showWindowButton, showWindowButton);\n                      setState(() {});\n                    },\n                    title: Text('使用系统标题栏',\n                        style: TextStyle(fontFamily: fontFamily)),\n                    description: Text('重启应用生效',\n                        style: TextStyle(fontFamily: fontFamily)),\n                    initialValue: showWindowButton,\n                  ),\n                ],\n              ),\n            if (Platform.isAndroid)\n              SettingsSection(\n                tiles: [\n                  SettingsTile.navigation(\n                    onPressed: (_) async {\n                      Modular.to.pushNamed('/settings/theme/display');\n                    },\n                    title:\n                        Text('屏幕帧率', style: TextStyle(fontFamily: fontFamily)),\n                  ),\n                ],\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/timeline/timeline_controller.dart",
    "content": "import 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:kazumi/utils/anime_season.dart';\nimport 'package:kazumi/repositories/collect_repository.dart';\nimport 'package:kazumi/modules/collect/collect_type.dart';\nimport 'package:mobx/mobx.dart';\n\npart 'timeline_controller.g.dart';\n\nclass TimelineController = _TimelineController with _$TimelineController;\n\nabstract class _TimelineController with Store {\n  final _collectRepository = Modular.get<ICollectRepository>();\n\n  @observable\n  ObservableList<List<BangumiItem>> bangumiCalendar =\n      ObservableList<List<BangumiItem>>();\n\n  @observable\n  String seasonString = '';\n\n  @observable\n  bool isLoading = false;\n\n  @observable\n  bool isTimeOut = false;\n\n  @observable\n  late bool notShowAbandonedBangumis = _collectRepository.getTimelineNotShowAbandonedBangumis();\n\n  @observable\n  late bool notShowWatchedBangumis = _collectRepository.getTimelineNotShowWatchedBangumis();\n\n  int sortType = 1;\n\n  late DateTime selectedDate;\n\n  void init() {\n    selectedDate = DateTime.now();\n    seasonString = AnimeSeason(selectedDate).toString();\n    getSchedules();\n  }\n\n  Future<void> getSchedules() async {\n    isLoading = true;\n    isTimeOut = false;\n    bangumiCalendar.clear();\n    final resBangumiCalendar = await BangumiHTTP.getCalendar();\n    bangumiCalendar.clear();\n    bangumiCalendar.addAll(resBangumiCalendar);\n    changeSortType(sortType);\n    isLoading = false;\n    isTimeOut = bangumiCalendar.isEmpty;\n  }\n\n  Future<void> getSchedulesBySeason() async {\n    // 4次获取，每次最多20部\n    isLoading = true;\n    isTimeOut = false;\n    bangumiCalendar.clear();\n    var time = 0;\n    const maxTime = 4;\n    const limit = 20;\n    var resBangumiCalendar = List.generate(7, (_) => <BangumiItem>[]);\n    for (time = 0; time < maxTime; time++) {\n      final offset = time * limit;\n      var newList = await BangumiHTTP.getCalendarBySearch(\n          AnimeSeason(selectedDate).toSeasonStartAndEnd(), limit, offset);\n      for (int i = 0; i < resBangumiCalendar.length; ++i) {\n        resBangumiCalendar[i].addAll(newList[i]);\n      }\n      bangumiCalendar.clear();\n      bangumiCalendar.addAll(resBangumiCalendar);\n    }\n    isLoading = false;\n    if (bangumiCalendar.isEmpty) {\n      isTimeOut = true;\n    } else {\n      isTimeOut = bangumiCalendar.every((innerList) => innerList.isEmpty);\n    }\n    if (!isTimeOut) {\n      changeSortType(sortType);\n    }\n  }\n\n  void tryEnterSeason(DateTime date) {\n    selectedDate = date;\n    seasonString = \"加载中 ٩(◦`꒳´◦)۶\";\n  }\n\n  /// 排序方式\n  /// 1. default\n  /// 2. score\n  /// 3. heat\n  void changeSortType(int type) {\n    if (type < 1 || type > 3) {\n      return;\n    }\n    sortType = type;\n    var resBangumiCalendar = bangumiCalendar.toList();\n    for (var dayList in resBangumiCalendar) {\n      switch (sortType) {\n        case 1:\n          dayList.sort((a, b) => a.id.compareTo(b.id));\n          break;\n        case 2:\n          dayList.sort((a, b) => (b.ratingScore).compareTo(a.ratingScore));\n          break;\n        case 3:\n          dayList.sort((a, b) => (b.votes).compareTo(a.votes));\n          break;\n        default:\n      }\n    }\n    bangumiCalendar.clear();\n    bangumiCalendar.addAll(resBangumiCalendar);\n  }\n\n  @action\n  Future<void> setNotShowAbandonedBangumis(bool value) async {\n    notShowAbandonedBangumis = value;\n    await _collectRepository.updateTimelineNotShowAbandonedBangumis(value);\n  }\n\n  @action\n  Future<void> setNotShowWatchedBangumis(bool value) async {\n    notShowWatchedBangumis = value;\n    await _collectRepository.updateTimelineNotShowWatchedBangumis(value);\n  }\n\n  Set<int> loadAbandonedBangumiIds() {\n    return _collectRepository.getBangumiIdsByType(CollectType.abandoned);\n  }\n\n  Set<int> loadWatchedBangumiIds() {\n    return _collectRepository.getBangumiIdsByType(CollectType.watched);\n  }\n}\n"
  },
  {
    "path": "lib/pages/timeline/timeline_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'timeline_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$TimelineController on _TimelineController, Store {\n  late final _$bangumiCalendarAtom =\n      Atom(name: '_TimelineController.bangumiCalendar', context: context);\n\n  @override\n  ObservableList<List<BangumiItem>> get bangumiCalendar {\n    _$bangumiCalendarAtom.reportRead();\n    return super.bangumiCalendar;\n  }\n\n  @override\n  set bangumiCalendar(ObservableList<List<BangumiItem>> value) {\n    _$bangumiCalendarAtom.reportWrite(value, super.bangumiCalendar, () {\n      super.bangumiCalendar = value;\n    });\n  }\n\n  late final _$seasonStringAtom =\n      Atom(name: '_TimelineController.seasonString', context: context);\n\n  @override\n  String get seasonString {\n    _$seasonStringAtom.reportRead();\n    return super.seasonString;\n  }\n\n  @override\n  set seasonString(String value) {\n    _$seasonStringAtom.reportWrite(value, super.seasonString, () {\n      super.seasonString = value;\n    });\n  }\n\n  late final _$isLoadingAtom =\n      Atom(name: '_TimelineController.isLoading', context: context);\n\n  @override\n  bool get isLoading {\n    _$isLoadingAtom.reportRead();\n    return super.isLoading;\n  }\n\n  @override\n  set isLoading(bool value) {\n    _$isLoadingAtom.reportWrite(value, super.isLoading, () {\n      super.isLoading = value;\n    });\n  }\n\n  late final _$isTimeOutAtom =\n      Atom(name: '_TimelineController.isTimeOut', context: context);\n\n  @override\n  bool get isTimeOut {\n    _$isTimeOutAtom.reportRead();\n    return super.isTimeOut;\n  }\n\n  @override\n  set isTimeOut(bool value) {\n    _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () {\n      super.isTimeOut = value;\n    });\n  }\n\n  late final _$notShowAbandonedBangumisAtom = Atom(\n      name: '_TimelineController.notShowAbandonedBangumis', context: context);\n\n  @override\n  bool get notShowAbandonedBangumis {\n    _$notShowAbandonedBangumisAtom.reportRead();\n    return super.notShowAbandonedBangumis;\n  }\n\n  bool _notShowAbandonedBangumisIsInitialized = false;\n\n  @override\n  set notShowAbandonedBangumis(bool value) {\n    _$notShowAbandonedBangumisAtom.reportWrite(\n        value,\n        _notShowAbandonedBangumisIsInitialized\n            ? super.notShowAbandonedBangumis\n            : null, () {\n      super.notShowAbandonedBangumis = value;\n      _notShowAbandonedBangumisIsInitialized = true;\n    });\n  }\n\n  late final _$notShowWatchedBangumisAtom = Atom(\n      name: '_TimelineController.notShowWatchedBangumis', context: context);\n\n  @override\n  bool get notShowWatchedBangumis {\n    _$notShowWatchedBangumisAtom.reportRead();\n    return super.notShowWatchedBangumis;\n  }\n\n  bool _notShowWatchedBangumisIsInitialized = false;\n\n  @override\n  set notShowWatchedBangumis(bool value) {\n    _$notShowWatchedBangumisAtom.reportWrite(\n        value,\n        _notShowWatchedBangumisIsInitialized\n            ? super.notShowWatchedBangumis\n            : null, () {\n      super.notShowWatchedBangumis = value;\n      _notShowWatchedBangumisIsInitialized = true;\n    });\n  }\n\n  late final _$setNotShowAbandonedBangumisAsyncAction = AsyncAction(\n      '_TimelineController.setNotShowAbandonedBangumis',\n      context: context);\n\n  @override\n  Future<void> setNotShowAbandonedBangumis(bool value) {\n    return _$setNotShowAbandonedBangumisAsyncAction\n        .run(() => super.setNotShowAbandonedBangumis(value));\n  }\n\n  late final _$setNotShowWatchedBangumisAsyncAction = AsyncAction(\n      '_TimelineController.setNotShowWatchedBangumis',\n      context: context);\n\n  @override\n  Future<void> setNotShowWatchedBangumis(bool value) {\n    return _$setNotShowWatchedBangumisAsyncAction\n        .run(() => super.setNotShowWatchedBangumis(value));\n  }\n\n  @override\n  String toString() {\n    return '''\nbangumiCalendar: ${bangumiCalendar},\nseasonString: ${seasonString},\nisLoading: ${isLoading},\nisTimeOut: ${isTimeOut},\nnotShowAbandonedBangumis: ${notShowAbandonedBangumis},\nnotShowWatchedBangumis: ${notShowWatchedBangumis}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/timeline/timeline_module.dart",
    "content": "import 'package:kazumi/pages/timeline/timeline_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass TimelineModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const TimelinePage());\n  }\n}\n"
  },
  {
    "path": "lib/pages/timeline/timeline_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/menu/menu.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/pages/timeline/timeline_controller.dart';\nimport 'package:kazumi/bean/card/bangumi_timeline_card.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:provider/provider.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/anime_season.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/bean/widget/error_widget.dart';\n\nclass TimelinePage extends StatefulWidget {\n  const TimelinePage({super.key});\n\n  @override\n  State<TimelinePage> createState() => _TimelinePageState();\n}\n\nclass _TimelinePageState extends State<TimelinePage>\n    with SingleTickerProviderStateMixin {\n  final TimelineController timelineController =\n      Modular.get<TimelineController>();\n  late NavigationBarState navigationBarState;\n  TabController? tabController;\n  late bool showRating;\n\n  final List<Tab> optionTabs = [\n    Tab(text: \"排序方式\"),\n    Tab(text: \"过滤器\"),\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    int weekday = DateTime.now().weekday - 1;\n    tabController =\n        TabController(vsync: this, length: tabs.length, initialIndex: weekday);\n    navigationBarState =\n        Provider.of<NavigationBarState>(context, listen: false);\n    showRating = GStorage.setting.get(SettingBoxKey.showRating, defaultValue: true);\n    if (timelineController.bangumiCalendar.isEmpty) {\n      timelineController.init();\n    }\n  }\n\n  @override\n  void dispose() {\n    tabController?.dispose();\n    super.dispose();\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n    navigationBarState.updateSelectedIndex(0);\n    Modular.to.navigate('/tab/popular/');\n  }\n\n  DateTime generateDateTime(int year, String season) {\n    switch (season) {\n      case '冬':\n        return DateTime(year, 1, 1);\n      case '春':\n        return DateTime(year, 4, 1);\n      case '夏':\n        return DateTime(year, 7, 1);\n      case '秋':\n        return DateTime(year, 10, 1);\n      default:\n        return DateTime.now();\n    }\n  }\n\n  final List<Tab> tabs = const <Tab>[\n    Tab(text: '一'),\n    Tab(text: '二'),\n    Tab(text: '三'),\n    Tab(text: '四'),\n    Tab(text: '五'),\n    Tab(text: '六'),\n    Tab(text: '日'),\n  ];\n\n  final seasons = ['秋', '夏', '春', '冬'];\n\n  String getStringByDateTime(DateTime d) {\n    return d.year.toString() + Utils.getSeasonStringByMonth(d.month);\n  }\n\n  void showSeasonBottomSheet(BuildContext context) {\n    final currDate = DateTime.now();\n    final years = List.generate(20, (index) => currDate.year - index);\n\n    // 按年份分组生成可用季节\n    Map<int, List<DateTime>> yearSeasons = {};\n    for (final year in years) {\n      List<DateTime> availableSeasons = [];\n      for (final season in seasons) {\n        final date = generateDateTime(year, season);\n        if (currDate.isAfter(date)) {\n          availableSeasons.add(date);\n        }\n      }\n      if (availableSeasons.isNotEmpty) {\n        yearSeasons[year] = availableSeasons;\n      }\n    }\n\n    KazumiDialog.showBottomSheet(\n      // context: context,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      shape: const RoundedRectangleBorder(\n        borderRadius: BorderRadius.vertical(top: Radius.circular(28)),\n      ),\n      isScrollControlled: true,\n      builder: (BuildContext context) {\n        return DraggableScrollableSheet(\n          initialChildSize: 0.6,\n          minChildSize: 0.3,\n          maxChildSize: 0.9,\n          expand: false,\n          builder: (context, scrollController) {\n            return Container(\n              decoration: BoxDecoration(\n                color: Theme.of(context).colorScheme.surface,\n                borderRadius:\n                    const BorderRadius.vertical(top: Radius.circular(28)),\n              ),\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Padding(\n                    padding:\n                        const EdgeInsets.symmetric(horizontal: 24, vertical: 8),\n                    child: Row(\n                      children: [\n                        Icon(\n                          Icons.schedule,\n                          color: Theme.of(context).colorScheme.primary,\n                          size: 24,\n                        ),\n                        const SizedBox(width: 12),\n                        Text(\n                          '时间机器',\n                          style: Theme.of(context)\n                              .textTheme\n                              .titleLarge\n                              ?.copyWith(\n                                color: Theme.of(context).colorScheme.onSurface,\n                              ),\n                        ),\n                      ],\n                    ),\n                  ),\n                  Divider(\n                    height: 1,\n                    color: Theme.of(context)\n                        .colorScheme\n                        .outlineVariant\n                        .withValues(alpha: 0.5),\n                  ),\n                  // 年份季节列表\n                  Expanded(\n                    child: ListView.builder(\n                      controller: scrollController,\n                      padding: const EdgeInsets.symmetric(\n                          horizontal: 24, vertical: 16),\n                      itemCount: yearSeasons.keys.length,\n                      itemBuilder: (context, index) {\n                        final year = yearSeasons.keys.elementAt(index);\n                        final availableSeasons = yearSeasons[year]!;\n\n                        return Padding(\n                          padding: const EdgeInsets.only(bottom: 32),\n                          child: Column(\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            children: [\n                              // 年份标题\n                              Padding(\n                                padding: const EdgeInsets.only(bottom: 16),\n                                child: Row(\n                                  children: [\n                                    Container(\n                                      width: 4,\n                                      height: 20,\n                                      decoration: BoxDecoration(\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .primary,\n                                        borderRadius: BorderRadius.circular(2),\n                                      ),\n                                    ),\n                                    const SizedBox(width: 12),\n                                    Text(\n                                      '$year年',\n                                      style: Theme.of(context)\n                                          .textTheme\n                                          .titleMedium\n                                          ?.copyWith(\n                                            color: Theme.of(context)\n                                                .colorScheme\n                                                .onSurface,\n                                          ),\n                                    ),\n                                  ],\n                                ),\n                              ),\n                              // 季节选择器\n                              buildSeasonSegmentedButton(\n                                  context, availableSeasons),\n                            ],\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                ],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget buildSeasonSegmentedButton(\n      BuildContext context, List<DateTime> availableSeasons) {\n    DateTime? selectedSeason;\n    for (final season in availableSeasons) {\n      if (Utils.isSameSeason(timelineController.selectedDate, season)) {\n        selectedSeason = season;\n        break;\n      }\n    }\n\n    final segments = availableSeasons.map((date) {\n      final seasonName = Utils.getSeasonStringByMonth(date.month);\n      return ButtonSegment<DateTime>(\n        value: date,\n        label: Text(\n          seasonName,\n          style: Theme.of(context).textTheme.labelLarge?.copyWith(\n                fontWeight: FontWeight.w600,\n              ),\n        ),\n        icon: getSeasonIcon(seasonName),\n      );\n    }).toList();\n\n    return SizedBox(\n      width: double.infinity,\n      child: SegmentedButton<DateTime>(\n        segments: segments,\n        selected: selectedSeason != null ? {selectedSeason} : {},\n        onSelectionChanged: (Set<DateTime> newSelection) {\n          if (newSelection.isNotEmpty) {\n            Navigator.pop(context);\n            onSeasonSelected(newSelection.first);\n          }\n        },\n        multiSelectionEnabled: false,\n        showSelectedIcon: false,\n        emptySelectionAllowed: true,\n        style: SegmentedButton.styleFrom(\n          backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,\n          foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant,\n          selectedForegroundColor:\n              Theme.of(context).colorScheme.onSecondaryContainer,\n          selectedBackgroundColor:\n              Theme.of(context).colorScheme.secondaryContainer,\n          side: BorderSide(\n            color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),\n            width: 1,\n          ),\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(20),\n          ),\n          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\n        ),\n      ),\n    );\n  }\n\n  Widget getSeasonIcon(String seasonName) {\n    IconData iconData;\n    switch (seasonName) {\n      case '春':\n        iconData = Icons.eco;\n        break;\n      case '夏':\n        iconData = Icons.wb_sunny;\n        break;\n      case '秋':\n        iconData = Icons.park;\n        break;\n      case '冬':\n        iconData = Icons.ac_unit;\n        break;\n      default:\n        iconData = Icons.schedule;\n    }\n\n    return Icon(\n      iconData,\n      size: 18,\n    );\n  }\n\n  void onSeasonSelected(DateTime date) async {\n    final currDate = DateTime.now();\n    timelineController.tryEnterSeason(date);\n\n    if (Utils.isSameSeason(timelineController.selectedDate, currDate)) {\n      await timelineController.getSchedules();\n    } else {\n      await timelineController.getSchedulesBySeason();\n    }\n\n    timelineController.seasonString =\n        AnimeSeason(timelineController.selectedDate).toString();\n  }\n\n  Widget showFilterSwitcher() {\n    return Wrap(\n      children: [\n        Observer(\n          builder: (context) => InkWell(\n            onTap: () {\n              timelineController.setNotShowAbandonedBangumis(\n                  !timelineController.notShowAbandonedBangumis);\n            },\n            child: ListTile(\n              title: const Text('不显示已抛弃的番剧'),\n              trailing: Switch(\n                value: timelineController.notShowAbandonedBangumis,\n                onChanged: (value) {\n                  timelineController.setNotShowAbandonedBangumis(value);\n                },\n              ),\n            ),\n          ),\n        ),\n        Observer(\n          builder: (context) => InkWell(\n            onTap: () {\n              timelineController.setNotShowWatchedBangumis(\n                  !timelineController.notShowWatchedBangumis);\n            },\n            child: ListTile(\n              title: const Text('不显示已看过的番剧'),\n              trailing: Switch(\n                value: timelineController.notShowWatchedBangumis,\n                onChanged: (value) {\n                  timelineController.setNotShowWatchedBangumis(value);\n                },\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget showSortSwitcher() {\n    return Wrap(\n      children: [\n        Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            ListTile(\n              title: const Text('按热度排序'),\n              onTap: () {\n                KazumiDialog.dismiss();\n                timelineController.changeSortType(3);\n              },\n            ),\n            ListTile(\n              title: const Text('按评分排序'),\n              onTap: () {\n                KazumiDialog.dismiss();\n                timelineController.changeSortType(2);\n              },\n            ),\n            ListTile(\n              title: const Text('按时间排序'),\n              onTap: () {\n                KazumiDialog.dismiss();\n                timelineController.changeSortType(1);\n              },\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget showTimelineOptionTabBar({required List<Widget> options}) {\n    return DefaultTabController(\n        length: optionTabs.length,\n        child: Scaffold(\n            body: Column(\n          children: [\n            PreferredSize(\n              preferredSize: Size.fromHeight(kToolbarHeight),\n              child: Material(\n                child: TabBar(\n                  tabs: optionTabs,\n                ),\n              ),\n            ),\n            Expanded(\n                child: TabBarView(\n              children: options,\n            ))\n          ],\n        )));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: SysAppBar(\n          needTopOffset: false,\n          toolbarHeight: 104,\n          bottom: TabBar(\n            controller: tabController,\n            tabs: tabs,\n            indicatorColor: Theme.of(context).colorScheme.primary,\n          ),\n          title: InkWell(\n            borderRadius: BorderRadius.circular(8),\n            child: Observer(builder: (context) {\n              return Text(timelineController.seasonString);\n            }),\n            onTap: () {\n              showSeasonBottomSheet(context);\n            },\n          ),\n        ),\n        floatingActionButton: FloatingActionButton(\n          onPressed: () async {\n            KazumiDialog.showBottomSheet(\n              backgroundColor: Theme.of(context).colorScheme.surface,\n              shape: const RoundedRectangleBorder(\n                borderRadius: BorderRadius.vertical(top: Radius.circular(28)),\n              ),\n              isScrollControlled: true,\n              constraints: BoxConstraints(\n                maxHeight: (MediaQuery.sizeOf(context).height >=\n                        LayoutBreakpoint.compact['height']!)\n                    ? MediaQuery.of(context).size.height * 1 / 4\n                    : MediaQuery.of(context).size.height,\n                maxWidth: (MediaQuery.sizeOf(context).width >=\n                        LayoutBreakpoint.medium['width']!)\n                    ? MediaQuery.of(context).size.width * 9 / 16\n                    : MediaQuery.of(context).size.width,\n              ),\n              clipBehavior: Clip.antiAlias,\n              context: context,\n              builder: (context) {\n                return showTimelineOptionTabBar(\n                    options: [showSortSwitcher(), showFilterSwitcher()]);\n              },\n            );\n          },\n          child: const Icon(Icons.tune),\n        ),\n        body: Observer(builder: (context) {\n          if (timelineController.isLoading &&\n              timelineController.bangumiCalendar.isEmpty) {\n            return const Center(\n              child: CircularProgressIndicator(),\n            );\n          }\n          if (timelineController.isTimeOut) {\n            return Center(\n              child: SizedBox(\n                height: 400,\n                child: GeneralErrorWidget(errMsg: '什么都没有找到 (´;ω;`)', actions: [\n                  GeneralErrorButton(\n                    onPressed: () {\n                      onSeasonSelected(timelineController.selectedDate);\n                    },\n                    text: '点击重试',\n                  ),\n                ]),\n              ),\n            );\n          }\n          return TabBarView(\n            controller: tabController,\n            children: contentGrid(timelineController.bangumiCalendar),\n          );\n        }),\n      ),\n    );\n  }\n\n  List<Widget> contentGrid(List<List<BangumiItem>> bangumiCalendar) {\n    List<Widget> gridViewList = [];\n    int crossCount = 1;\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {\n      crossCount = 2;\n    }\n    if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {\n      crossCount = 3;\n    }\n    double cardHeight =\n        Utils.isDesktop() ? 160 : (Utils.isTablet() ? 140 : 120);\n    for (var bangumiList in bangumiCalendar) {\n      // 根据过滤器设置过滤番剧\n      var filteredList = bangumiList;\n\n      if (timelineController.notShowAbandonedBangumis) {\n        final abandonedBangumiIds =\n            timelineController.loadAbandonedBangumiIds();\n        filteredList = filteredList\n            .where((item) => !abandonedBangumiIds.contains(item.id))\n            .toList();\n      }\n\n      if (timelineController.notShowWatchedBangumis) {\n        final watchedBangumiIds = timelineController.loadWatchedBangumiIds();\n        filteredList = filteredList\n            .where((item) => !watchedBangumiIds.contains(item.id))\n            .toList();\n      }\n\n      gridViewList.add(\n        CustomScrollView(\n          slivers: [\n            SliverGrid(\n              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n                mainAxisSpacing: StyleString.cardSpace - 2,\n                crossAxisSpacing: StyleString.cardSpace,\n                crossAxisCount: crossCount,\n                mainAxisExtent: cardHeight + 12,\n              ),\n              delegate: SliverChildBuilderDelegate(\n                (BuildContext context, int index) {\n                  if (filteredList.isEmpty) return null;\n                  final item = filteredList[index];\n                  return BangumiTimelineCard(\n                      bangumiItem: item,\n                      cardHeight: cardHeight,\n                      showRating: showRating);\n                },\n                childCount: filteredList.isNotEmpty ? filteredList.length : 10,\n              ),\n            ),\n          ],\n        ),\n      );\n    }\n    return gridViewList;\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/video_controller.dart",
    "content": "import 'dart:async';\nimport 'package:kazumi/modules/roads/road_module.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/repositories/download_repository.dart';\nimport 'package:kazumi/utils/download_manager.dart';\nimport 'package:kazumi/providers/video/providers.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:mobx/mobx.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:kazumi/modules/bangumi/episode_item.dart';\nimport 'package:kazumi/modules/comments/comment_item.dart';\nimport 'package:kazumi/request/bangumi.dart';\nimport 'package:dio/dio.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\n\npart 'video_controller.g.dart';\n\nclass VideoPageController = _VideoPageController with _$VideoPageController;\n\nabstract class _VideoPageController with Store {\n  late BangumiItem bangumiItem;\n  EpisodeInfo episodeInfo = EpisodeInfo.fromTemplate();\n\n  @observable\n  var episodeCommentsList = ObservableList<EpisodeCommentItem>();\n\n  @observable\n  bool loading = true;\n\n  @observable\n  String? errorMessage;\n\n  @observable\n  int currentEpisode = 1;\n\n  @observable\n  int currentRoad = 0;\n\n  /// 全屏状态\n  @observable\n  bool isFullscreen = false;\n\n  /// 评论正序或倒序\n  @observable\n  bool isCommentsAscending = false;\n\n  /// 画中画状态\n  @observable\n  bool isPip = false;\n\n  /// 播放列表显示状态\n  @observable\n  bool showTabBody = true;\n\n  /// 上次观看位置\n  @observable\n  int historyOffset = 0;\n\n  /// 离线播放模式\n  @observable\n  bool isOfflineMode = false;\n\n  /// 离线视频本地路径\n  String? _offlineVideoPath;\n\n  /// 和 bangumiItem 中的标题不同，此标题来自于视频源\n  String title = '';\n\n  String src = '';\n\n  @observable\n  var roadList = ObservableList<Road>();\n\n  late Plugin currentPlugin;\n\n  /// 离线模式下的虚拟插件名\n  String _offlinePluginName = '';\n\n  /// 用于取消正在进行的 queryRoads 操作\n  CancelToken? _queryRoadsCancelToken;\n\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  final HistoryController historyController = Modular.get<HistoryController>();\n  final IDownloadRepository downloadRepository =\n      Modular.get<IDownloadRepository>();\n  final IDownloadManager downloadManager = Modular.get<IDownloadManager>();\n  final Box setting = GStorage.setting;\n\n  /// 长生命周期的视频源提供者（页面生命周期内复用，WebView 实例在 Provider 内复用）\n  WebViewVideoSourceProvider? _videoSourceProvider;\n\n  /// 视频提供者日志流控制器\n  final StreamController<String> _logStreamController =\n      StreamController<String>.broadcast();\n\n  Stream<String> get logStream => _logStreamController.stream;\n\n  StreamSubscription<String>? _logSubscription;\n\n  /// 初始化离线播放模式\n  void initForOfflinePlayback({\n    required BangumiItem bangumiItem,\n    required String pluginName,\n    required int episodeNumber,\n    required String episodeName,\n    required int road,\n    required String videoPath,\n    required List<DownloadEpisode> downloadedEpisodes,\n  }) {\n    this.bangumiItem = bangumiItem;\n    _offlinePluginName = pluginName;\n    currentRoad = road;\n    title =\n        bangumiItem.nameCn.isNotEmpty ? bangumiItem.nameCn : bangumiItem.name;\n    isOfflineMode = true;\n    _offlineVideoPath = videoPath;\n    // 离线模式不需要解析视频源，直接设置 loading 为 false\n    loading = false;\n\n    // 构建仅包含已下载集数的 roadList\n    _buildOfflineRoadList(downloadedEpisodes);\n\n    // 离线模式下 roadList 长度为 1 , currentRoad 可能访问越界，需要校正\n    if (currentRoad < 0 || currentRoad >= roadList.length) {\n      currentRoad = 0;\n    }\n\n    // currentEpisode 是列表中的 1-based 位置，而非实际集数编号\n    // 在 roadList.data 中查找 episodeNumber 对应的位置\n    final index = roadList[currentRoad].data.indexOf(episodeNumber.toString());\n    currentEpisode = index >= 0 ? index + 1 : 1;\n    KazumiLogger().i(\n        'VideoPageController: initialized for offline playback, episode $episodeNumber (position: $currentEpisode)');\n  }\n\n  /// 构建离线模式的 roadList\n  void _buildOfflineRoadList(List<DownloadEpisode> episodes) {\n    roadList.clear();\n    episodes.sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber));\n    // 使用 '播放列表1' 作为名称，与 UI 代码兼容\n    roadList.add(Road(\n      name: '播放列表1',\n      // data 存储实际的 episodeNumber（字符串形式），用于离线播放时查找本地文件\n      data: episodes.map((e) => e.episodeNumber.toString()).toList(),\n      identifier: episodes\n          .map((e) =>\n              e.episodeName.isNotEmpty ? e.episodeName : '第${e.episodeNumber}集')\n          .toList(),\n    ));\n  }\n\n  void resetOfflineMode() {\n    isOfflineMode = false;\n    _offlineVideoPath = null;\n    _offlinePluginName = '';\n  }\n\n  String? get offlineVideoPath => _offlineVideoPath;\n\n  String get offlinePluginName => _offlinePluginName;\n\n  /// 获取当前实际的集数编号\n  /// 在线模式下直接返回 currentEpisode\n  /// 离线模式下从 roadList.data 中获取实际的 episodeNumber\n  int get actualEpisodeNumber {\n    if (isOfflineMode && roadList.isNotEmpty) {\n      try {\n        return int.parse(roadList[currentRoad].data[currentEpisode - 1]);\n      } catch (_) {\n        return currentEpisode;\n      }\n    }\n    return currentEpisode;\n  }\n\n  Future<void> changeEpisode(int episode,\n      {int currentRoad = 0, int offset = 0}) async {\n    currentEpisode = episode;\n    this.currentRoad = currentRoad;\n    errorMessage = null;\n\n    if (isOfflineMode) {\n      await _changeOfflineEpisode(episode, 0);\n      return;\n    }\n\n    String chapterName = roadList[currentRoad].identifier[episode - 1];\n    KazumiLogger().i('VideoPageController: changed to $chapterName');\n    String urlItem = roadList[currentRoad].data[episode - 1];\n    if (urlItem.contains(currentPlugin.baseUrl) ||\n        urlItem.contains(currentPlugin.baseUrl.replaceAll('https', 'http'))) {\n      urlItem = urlItem;\n    } else {\n      urlItem = currentPlugin.baseUrl + urlItem;\n    }\n\n    await _resolveWithProvider(urlItem, offset);\n  }\n\n  /// 离线模式下切换集数\n  /// [episode] 是列表中的位置（从 1 开始），需要从 roadList.data 中获取实际的 episodeNumber\n  Future<void> _changeOfflineEpisode(int episode, int offset) async {\n    // 从 roadList.data 中获取实际的 episodeNumber\n    final actualEpisodeNumber =\n        int.tryParse(roadList[currentRoad].data[episode - 1]);\n    if (actualEpisodeNumber == null) {\n      KazumiLogger().e(\n          'VideoPageController: failed to parse episode number from roadList data: ${roadList[currentRoad].data[episode - 1]}');\n      KazumiDialog.showToast(message: '集数解析失败');\n      return;\n    }\n\n    final localPath = _getLocalVideoPath(\n      bangumiItem.id,\n      _offlinePluginName,\n      actualEpisodeNumber,\n    );\n    if (localPath == null) {\n      KazumiDialog.showToast(message: '该集数未下载');\n      return;\n    }\n    _offlineVideoPath = localPath;\n    loading = false;\n\n    KazumiLogger().i(\n        'VideoPageController: offline episode changed to $actualEpisodeNumber (index: $episode), path: $localPath');\n\n    final params = PlaybackInitParams(\n      videoUrl: localPath,\n      offset: offset,\n      isLocalPlayback: true,\n      bangumiId: bangumiItem.id,\n      pluginName: _offlinePluginName,\n      episode: actualEpisodeNumber,\n      httpHeaders: {},\n      adBlockerEnabled: false,\n      episodeTitle: roadList[currentRoad].identifier[episode - 1],\n      referer: '',\n      currentRoad: currentRoad,\n    );\n\n    final playerController = Modular.get<PlayerController>();\n    await playerController.init(params);\n  }\n\n  /// 获取本地视频路径\n  String? _getLocalVideoPath(\n      int bangumiId, String pluginName, int episodeNumber) {\n    final episode =\n        downloadRepository.getEpisode(bangumiId, pluginName, episodeNumber);\n    return downloadManager.getLocalVideoPath(episode);\n  }\n\n  /// 使用 VideoSourceProvider 解析视频源\n  Future<void> _resolveWithProvider(String url, int offset) async {\n    _videoSourceProvider?.cancel();\n\n    loading = true;\n    _videoSourceProvider ??= WebViewVideoSourceProvider();\n\n    await _logSubscription?.cancel();\n    _logSubscription = _videoSourceProvider!.onLog.listen((log) {\n      if (!_logStreamController.isClosed) {\n        _logStreamController.add(log);\n      }\n    });\n\n    try {\n      final source = await _videoSourceProvider!.resolve(\n        url,\n        useLegacyParser: currentPlugin.useLegacyParser,\n        offset: offset,\n      );\n\n      loading = false;\n      KazumiLogger()\n          .i('VideoPageController: resolved video URL: ${source.url}');\n\n      final bool forceAdBlocker =\n          setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false);\n\n      final params = PlaybackInitParams(\n        videoUrl: source.url,\n        offset: source.offset,\n        isLocalPlayback: false,\n        bangumiId: bangumiItem.id,\n        pluginName: currentPlugin.name,\n        episode: currentEpisode,\n        httpHeaders: {\n          'user-agent': currentPlugin.userAgent.isEmpty\n              ? Utils.getRandomUA()\n              : currentPlugin.userAgent,\n          if (currentPlugin.referer.isNotEmpty)\n            'referer': currentPlugin.referer,\n        },\n        adBlockerEnabled: forceAdBlocker || currentPlugin.adBlocker,\n        episodeTitle: roadList[currentRoad].identifier[currentEpisode - 1],\n        referer: currentPlugin.referer,\n        currentRoad: currentRoad,\n      );\n\n      final playerController = Modular.get<PlayerController>();\n      await playerController.init(params);\n    } on VideoSourceTimeoutException {\n      loading = false;\n      errorMessage = '视频解析超时，请重试';\n    } on VideoSourceCancelledException {\n      KazumiLogger().i('VideoPageController: video URL resolution cancelled');\n      // 不设置 loading = false，因为可能是切换到新的集数\n    } catch (e) {\n      loading = false;\n      errorMessage = '视频解析失败：${e.toString()}';\n    }\n  }\n\n  /// 取消当前视频源解析并销毁 Provider（页面退出时调用）\n  void cancelVideoSourceResolution() {\n    _logSubscription?.cancel();\n    _logSubscription = null;\n    if (!_logStreamController.isClosed) {\n      _logStreamController.close();\n    }\n    _videoSourceProvider?.dispose();\n    _videoSourceProvider = null;\n  }\n\n  Future<void> queryBangumiEpisodeCommentsByID(int id, int episode) async {\n    episodeCommentsList.clear();\n    episodeInfo = await BangumiHTTP.getBangumiEpisodeByID(id, episode);\n    await BangumiHTTP.getBangumiCommentsByEpisodeID(episodeInfo.id)\n        .then((value) {\n      episodeCommentsList.addAll(value.commentList);\n    });\n    if (!isCommentsAscending) {\n      episodeCommentsList\n          .sort((a, b) => b.comment.createdAt.compareTo(a.comment.createdAt));\n    } else {\n      episodeCommentsList\n          .sort((a, b) => a.comment.createdAt.compareTo(b.comment.createdAt));\n    }\n    KazumiLogger().i(\n        'VideoPageController: loaded comments list length ${episodeCommentsList.length}');\n  }\n\n  Future<void> queryRoads(String url, String pluginName,\n      {CancelToken? cancelToken}) async {\n    if (cancelToken != null) {\n      _queryRoadsCancelToken?.cancel();\n      _queryRoadsCancelToken = cancelToken;\n    } else {\n      _queryRoadsCancelToken?.cancel();\n      _queryRoadsCancelToken = CancelToken();\n      cancelToken = _queryRoadsCancelToken;\n    }\n\n    final PluginsController pluginsController =\n        Modular.get<PluginsController>();\n    roadList.clear();\n    for (Plugin plugin in pluginsController.pluginList) {\n      if (plugin.name == pluginName) {\n        roadList.addAll(\n            await plugin.querychapterRoads(url, cancelToken: cancelToken));\n      }\n    }\n    KazumiLogger()\n        .i('VideoPageController: road list length ${roadList.length}');\n    KazumiLogger().i(\n        'VideoPageController: first road episode count ${roadList[0].data.length}');\n  }\n\n  void toggleSortOrder() {\n    isCommentsAscending = !isCommentsAscending;\n    episodeCommentsList.sort(\n      (a, b) => isCommentsAscending\n          ? a.comment.createdAt.compareTo(b.comment.createdAt)\n          : b.comment.createdAt.compareTo(a.comment.createdAt),\n    );\n  }\n\n  void cancelQueryRoads() {\n    if (_queryRoadsCancelToken != null) {\n      if (!_queryRoadsCancelToken!.isCancelled) {\n        _queryRoadsCancelToken!.cancel();\n      }\n    }\n  }\n\n  void enterFullScreen() {\n    isFullscreen = true;\n    showTabBody = false;\n    Utils.enterFullScreen(lockOrientation: false);\n  }\n\n  void exitFullScreen() {\n    isFullscreen = false;\n    Utils.exitFullScreen();\n  }\n\n  void isDesktopFullscreen() async {\n    if (Utils.isDesktop()) {\n      isFullscreen = await windowManager.isFullScreen();\n    }\n  }\n\n  void handleOnEnterFullScreen() async {\n    isFullscreen = true;\n    showTabBody = false;\n  }\n\n  void handleOnExitFullScreen() async {\n    isFullscreen = false;\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/video_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'video_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$VideoPageController on _VideoPageController, Store {\n  late final _$episodeCommentsListAtom =\n      Atom(name: '_VideoPageController.episodeCommentsList', context: context);\n\n  @override\n  ObservableList<EpisodeCommentItem> get episodeCommentsList {\n    _$episodeCommentsListAtom.reportRead();\n    return super.episodeCommentsList;\n  }\n\n  @override\n  set episodeCommentsList(ObservableList<EpisodeCommentItem> value) {\n    _$episodeCommentsListAtom.reportWrite(value, super.episodeCommentsList, () {\n      super.episodeCommentsList = value;\n    });\n  }\n\n  late final _$loadingAtom =\n      Atom(name: '_VideoPageController.loading', context: context);\n\n  @override\n  bool get loading {\n    _$loadingAtom.reportRead();\n    return super.loading;\n  }\n\n  @override\n  set loading(bool value) {\n    _$loadingAtom.reportWrite(value, super.loading, () {\n      super.loading = value;\n    });\n  }\n\n  late final _$errorMessageAtom =\n      Atom(name: '_VideoPageController.errorMessage', context: context);\n\n  @override\n  String? get errorMessage {\n    _$errorMessageAtom.reportRead();\n    return super.errorMessage;\n  }\n\n  @override\n  set errorMessage(String? value) {\n    _$errorMessageAtom.reportWrite(value, super.errorMessage, () {\n      super.errorMessage = value;\n    });\n  }\n\n  late final _$currentEpisodeAtom =\n      Atom(name: '_VideoPageController.currentEpisode', context: context);\n\n  @override\n  int get currentEpisode {\n    _$currentEpisodeAtom.reportRead();\n    return super.currentEpisode;\n  }\n\n  @override\n  set currentEpisode(int value) {\n    _$currentEpisodeAtom.reportWrite(value, super.currentEpisode, () {\n      super.currentEpisode = value;\n    });\n  }\n\n  late final _$currentRoadAtom =\n      Atom(name: '_VideoPageController.currentRoad', context: context);\n\n  @override\n  int get currentRoad {\n    _$currentRoadAtom.reportRead();\n    return super.currentRoad;\n  }\n\n  @override\n  set currentRoad(int value) {\n    _$currentRoadAtom.reportWrite(value, super.currentRoad, () {\n      super.currentRoad = value;\n    });\n  }\n\n  late final _$isFullscreenAtom =\n      Atom(name: '_VideoPageController.isFullscreen', context: context);\n\n  @override\n  bool get isFullscreen {\n    _$isFullscreenAtom.reportRead();\n    return super.isFullscreen;\n  }\n\n  @override\n  set isFullscreen(bool value) {\n    _$isFullscreenAtom.reportWrite(value, super.isFullscreen, () {\n      super.isFullscreen = value;\n    });\n  }\n\n  late final _$isCommentsAscendingAtom =\n      Atom(name: '_VideoPageController.isCommentsAscending', context: context);\n\n  @override\n  bool get isCommentsAscending {\n    _$isCommentsAscendingAtom.reportRead();\n    return super.isCommentsAscending;\n  }\n\n  @override\n  set isCommentsAscending(bool value) {\n    _$isCommentsAscendingAtom.reportWrite(value, super.isCommentsAscending, () {\n      super.isCommentsAscending = value;\n    });\n  }\n\n  late final _$isPipAtom =\n      Atom(name: '_VideoPageController.isPip', context: context);\n\n  @override\n  bool get isPip {\n    _$isPipAtom.reportRead();\n    return super.isPip;\n  }\n\n  @override\n  set isPip(bool value) {\n    _$isPipAtom.reportWrite(value, super.isPip, () {\n      super.isPip = value;\n    });\n  }\n\n  late final _$showTabBodyAtom =\n      Atom(name: '_VideoPageController.showTabBody', context: context);\n\n  @override\n  bool get showTabBody {\n    _$showTabBodyAtom.reportRead();\n    return super.showTabBody;\n  }\n\n  @override\n  set showTabBody(bool value) {\n    _$showTabBodyAtom.reportWrite(value, super.showTabBody, () {\n      super.showTabBody = value;\n    });\n  }\n\n  late final _$historyOffsetAtom =\n      Atom(name: '_VideoPageController.historyOffset', context: context);\n\n  @override\n  int get historyOffset {\n    _$historyOffsetAtom.reportRead();\n    return super.historyOffset;\n  }\n\n  @override\n  set historyOffset(int value) {\n    _$historyOffsetAtom.reportWrite(value, super.historyOffset, () {\n      super.historyOffset = value;\n    });\n  }\n\n  late final _$isOfflineModeAtom =\n      Atom(name: '_VideoPageController.isOfflineMode', context: context);\n\n  @override\n  bool get isOfflineMode {\n    _$isOfflineModeAtom.reportRead();\n    return super.isOfflineMode;\n  }\n\n  @override\n  set isOfflineMode(bool value) {\n    _$isOfflineModeAtom.reportWrite(value, super.isOfflineMode, () {\n      super.isOfflineMode = value;\n    });\n  }\n\n  late final _$roadListAtom =\n      Atom(name: '_VideoPageController.roadList', context: context);\n\n  @override\n  ObservableList<Road> get roadList {\n    _$roadListAtom.reportRead();\n    return super.roadList;\n  }\n\n  @override\n  set roadList(ObservableList<Road> value) {\n    _$roadListAtom.reportWrite(value, super.roadList, () {\n      super.roadList = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\nepisodeCommentsList: ${episodeCommentsList},\nloading: ${loading},\nerrorMessage: ${errorMessage},\ncurrentEpisode: ${currentEpisode},\ncurrentRoad: ${currentRoad},\nisFullscreen: ${isFullscreen},\nisCommentsAscending: ${isCommentsAscending},\nisPip: ${isPip},\nshowTabBody: ${showTabBody},\nhistoryOffset: ${historyOffset},\nisOfflineMode: ${isOfflineMode},\nroadList: ${roadList}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/video_module.dart",
    "content": "import 'package:kazumi/pages/video/video_page.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\n\nclass VideoModule extends Module {\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const VideoPage());\n  }\n\n  @override\n  void binds(i) {\n    i.addSingleton(PlayerController.new);\n  }\n}\n"
  },
  {
    "path": "lib/pages/video/video_page.dart",
    "content": "import 'dart:async';\nimport 'package:canvas_danmaku/models/danmaku_content_item.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/player/player_controller.dart';\nimport 'package:kazumi/pages/video/video_controller.dart';\nimport 'package:kazumi/pages/history/history_controller.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/pages/player/player_item.dart';\nimport 'package:flutter_mobx/flutter_mobx.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb;\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';\nimport 'package:scrollview_observer/scrollview_observer.dart';\nimport 'package:kazumi/pages/player/episode_comments_sheet.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:kazumi/bean/widget/embedded_native_control_area.dart';\nimport 'package:kazumi/pages/download/download_controller.dart';\nimport 'package:kazumi/pages/download/download_episode_sheet.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/utils/timed_shutdown_service.dart';\n\nclass VideoPage extends StatefulWidget {\n  const VideoPage({super.key});\n\n  @override\n  State<VideoPage> createState() => _VideoPageState();\n}\n\nclass _VideoPageState extends State<VideoPage>\n    with TickerProviderStateMixin, WindowListener {\n  Box setting = GStorage.setting;\n  final VideoPageController videoPageController =\n      Modular.get<VideoPageController>();\n  final PlayerController playerController = Modular.get<PlayerController>();\n  final HistoryController historyController = Modular.get<HistoryController>();\n  final DownloadController downloadController =\n      Modular.get<DownloadController>();\n  late bool playResume;\n  bool showDebugLog = false;\n  List<String> webviewLogLines = [];\n  StreamSubscription<String>? _logSubscription;\n  final FocusNode keyboardFocus = FocusNode();\n\n  ScrollController scrollController = ScrollController();\n  late GridObserverController observerController;\n  late AnimationController animation;\n  late Animation<Offset> _rightOffsetAnimation;\n  late Animation<double> _maskOpacityAnimation;\n  late TabController tabController;\n\n  // 当前播放列表\n  late int currentRoad;\n\n  // disable animation.\n  late final bool disableAnimations;\n\n  // SyncPlayChatMessage\n  late final StreamSubscription<SyncPlayChatMessage> _syncChatSubscription;\n\n  @override\n  void initState() {\n    super.initState();\n    windowManager.addListener(this);\n    // Check fullscreen when enter video page\n    // in case user use system controls to enter fullscreen outside video page\n    videoPageController.isDesktopFullscreen();\n    tabController = TabController(length: 2, vsync: this);\n    observerController = GridObserverController(controller: scrollController);\n    animation = AnimationController(\n      duration: const Duration(milliseconds: 120),\n      vsync: this,\n    );\n    _rightOffsetAnimation = Tween<Offset>(\n      begin: const Offset(1.0, 0.0),\n      end: const Offset(0.0, 0.0),\n    ).animate(CurvedAnimation(\n      parent: animation,\n      curve: Curves.easeOut,\n    ));\n    _maskOpacityAnimation = Tween<double>(\n      begin: 0.0,\n      end: 1.0,\n    ).animate(CurvedAnimation(\n      parent: animation,\n      curve: Curves.easeIn,\n    ));\n\n    playResume = setting.get(SettingBoxKey.playResume, defaultValue: true);\n    disableAnimations =\n        setting.get(SettingBoxKey.playerDisableAnimations, defaultValue: false);\n\n    if (videoPageController.isOfflineMode) {\n      // 离线模式：跳过 WebView 订阅，直接初始化播放器\n      _initOfflineMode();\n    } else {\n      // 在线模式：设置 WebView 订阅\n      _initOnlineMode();\n    }\n\n    _syncChatSubscription = playerController.syncPlayChatStream.listen((event) {\n      final localUsername = playerController.syncplayController?.username ?? '';\n      final String displayText = '${event.username}：${event.message}';\n\n      // 只有在弹幕开启时渲染弹幕并确保是别人发送的弹幕\n      if (playerController.danmakuOn &&\n          event.username != localUsername &&\n          event.fromRemote) {\n        playerController.danmakuController.addDanmaku(\n          DanmakuContentItem(\n            displayText,\n            color: Colors.orange,\n            isColorful: true,\n            type: DanmakuItemType.bottom,\n            extra: DateTime.now().millisecondsSinceEpoch,\n          ),\n        );\n      }\n    });\n  }\n\n  void _initOfflineMode() {\n    videoPageController.showTabBody = true;\n    videoPageController.historyOffset = 0;\n    currentRoad = videoPageController.currentRoad;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) async {\n      if (videoPageController.offlineVideoPath != null) {\n        final params = PlaybackInitParams(\n          videoUrl: videoPageController.offlineVideoPath!,\n          offset: videoPageController.historyOffset,\n          isLocalPlayback: true,\n          bangumiId: videoPageController.bangumiItem.id,\n          pluginName: videoPageController.offlinePluginName,\n          episode: videoPageController.actualEpisodeNumber,\n          httpHeaders: {},\n          adBlockerEnabled: false,\n          episodeTitle: videoPageController\n              .roadList[videoPageController.currentRoad]\n              .identifier[videoPageController.currentEpisode - 1],\n          referer: '',\n          currentRoad: videoPageController.currentRoad,\n        );\n        await playerController.init(params);\n      }\n    });\n  }\n\n  void _initOnlineMode() {\n    videoPageController.currentEpisode = 1;\n    videoPageController.currentRoad = 0;\n    videoPageController.historyOffset = 0;\n    videoPageController.showTabBody = true;\n\n    var progress = historyController.lastWatching(\n        videoPageController.bangumiItem,\n        videoPageController.currentPlugin.name);\n    if (progress != null) {\n      if (videoPageController.roadList.length > progress.road) {\n        if (videoPageController.roadList[progress.road].data.length >=\n            progress.episode) {\n          videoPageController.currentEpisode = progress.episode;\n          videoPageController.currentRoad = progress.road;\n          if (playResume) {\n            videoPageController.historyOffset = progress.progress.inSeconds;\n          }\n        }\n      }\n    }\n    currentRoad = videoPageController.currentRoad;\n\n    _logSubscription = videoPageController.logStream.listen((log) {\n      if (mounted) {\n        setState(() {\n          webviewLogLines.add(log);\n          if (webviewLogLines.length > 100) {\n            webviewLogLines.removeAt(0);\n          }\n        });\n      }\n    });\n\n    // 使用 Provider 模式启动播放\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      changeEpisode(videoPageController.currentEpisode,\n          currentRoad: videoPageController.currentRoad,\n          offset: videoPageController.historyOffset);\n    });\n  }\n\n  @override\n  void dispose() {\n    try {\n      windowManager.removeListener(this);\n    } catch (_) {}\n    try {\n      observerController.controller?.dispose();\n    } catch (_) {}\n    try {\n      animation.dispose();\n    } catch (_) {}\n    try {\n      _syncChatSubscription.cancel();\n    } catch (_) {}\n    try {\n      _logSubscription?.cancel();\n    } catch (_) {}\n    try {\n      playerController.dispose();\n    } catch (e) {\n      KazumiLogger().e(\n          'VideoPageController: failed to dispose playerController',\n          error: e);\n    }\n    // 取消正在进行的视频源解析\n    videoPageController.cancelVideoSourceResolution();\n    if (!Utils.isDesktop()) {\n      try {\n        ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness();\n      } catch (_) {}\n    }\n    videoPageController.episodeInfo.reset();\n    videoPageController.episodeCommentsList.clear();\n    // 重置离线模式\n    videoPageController.resetOfflineMode();\n    Utils.unlockScreenRotation();\n    tabController.dispose();\n    // Cancel timed shutdown when leaving anime page\n    TimedShutdownService().cancel();\n    super.dispose();\n  }\n\n  // Handle fullscreen change invoked by system controls\n  @override\n  void onWindowEnterFullScreen() {\n    videoPageController.handleOnEnterFullScreen();\n  }\n\n  @override\n  void onWindowLeaveFullScreen() {\n    videoPageController.handleOnExitFullScreen();\n  }\n\n  void showDebugConsole() {\n    setState(() {\n      showDebugLog = true;\n    });\n  }\n\n  void hideDebugConsole() {\n    setState(() {\n      showDebugLog = false;\n    });\n  }\n\n  void switchDebugConsole() {\n    setState(() {\n      showDebugLog = !showDebugLog;\n    });\n  }\n\n  void clearWebviewLog() {\n    setState(() {\n      webviewLogLines.clear();\n    });\n  }\n\n  Future<void> changeEpisode(int episode,\n      {int currentRoad = 0, int offset = 0}) async {\n    clearWebviewLog();\n    hideDebugConsole();\n    videoPageController.loading = true;\n    videoPageController.errorMessage = null;\n    videoPageController.episodeInfo.reset();\n    videoPageController.episodeCommentsList.clear();\n    await playerController.stop();\n    await videoPageController.changeEpisode(episode,\n        currentRoad: currentRoad, offset: offset);\n  }\n\n  void menuJumpToCurrentEpisode() {\n    Future.delayed(const Duration(milliseconds: 20), () async {\n      await observerController.jumpTo(\n          index: videoPageController.currentEpisode > 1\n              ? videoPageController.currentEpisode - 1\n              : videoPageController.currentEpisode);\n    });\n  }\n\n  void openTabBodyAnimated() {\n    if (videoPageController.showTabBody) {\n      if (!disableAnimations) {\n        animation.forward();\n      }\n      menuJumpToCurrentEpisode();\n    }\n  }\n\n  void closeTabBodyAnimated() {\n    if (!disableAnimations) {\n      animation.reverse();\n      Future.delayed(const Duration(milliseconds: 120), () {\n        videoPageController.showTabBody = false;\n      });\n    } else {\n      videoPageController.showTabBody = false;\n    }\n    keyboardFocus.requestFocus();\n  }\n\n  void onBackPressed(BuildContext context) async {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n    if (videoPageController.isPip) {\n      Utils.exitDesktopPIPWindow();\n      videoPageController.isPip = false;\n      return;\n    }\n    if (videoPageController.isFullscreen && !Utils.isTablet()) {\n      menuJumpToCurrentEpisode();\n      await Utils.exitFullScreen();\n      videoPageController.showTabBody = false;\n      videoPageController.isFullscreen = false;\n      return;\n    }\n    if (videoPageController.isFullscreen) {\n      Utils.exitFullScreen();\n      videoPageController.isFullscreen = false;\n    }\n    Navigator.of(context).pop();\n  }\n\n  /// Callback for timed shutdown - pauses video when timer expires\n  void pauseForTimedShutdown() {\n    if (playerController.playing) {\n      playerController.pause();\n    }\n  }\n\n  /// 发送弹幕 由于接口限制, 暂时未提交云端\n  void sendDanmaku(String msg) async {\n    keyboardFocus.requestFocus();\n    if (playerController.danDanmakus.isEmpty) {\n      KazumiDialog.showToast(\n        message: '当前剧集不支持弹幕发送的说',\n      );\n      return;\n    }\n    if (msg.isEmpty) {\n      KazumiDialog.showToast(message: '弹幕内容为空');\n      return;\n    } else if (msg.length > 100) {\n      KazumiDialog.showToast(message: '弹幕内容过长');\n      return;\n    }\n\n    final destination = playerController.danmakuDestination;\n\n    if (destination == DanmakuDestination.chatRoom) {\n      if (playerController.syncplayRoom.isEmpty) {\n        KazumiDialog.showToast(message: '你还没有加入一起看，无法发送聊天室弹幕');\n        return;\n      }\n\n      final sender = playerController.syncplayController?.username ?? '我';\n      final String displayText = '$sender：$msg';\n\n      // 在播放器渲染自己发送的弹幕\n      playerController.danmakuController.addDanmaku(\n        DanmakuContentItem(\n          displayText,\n          color: Colors.orange,\n          isColorful: true,\n          type: DanmakuItemType.bottom,\n          extra: DateTime.now().millisecondsSinceEpoch,\n        ),\n      );\n\n      // 发送弹幕到聊天室\n      playerController.sendSyncPlayChatMessage(msg);\n    } else {\n      // Todo 接口方限制\n\n      playerController.danmakuController\n          .addDanmaku(DanmakuContentItem(msg, selfSend: true));\n    }\n  }\n\n  void showMobileDanmakuInput() {\n    final TextEditingController textController = TextEditingController();\n    showModalBottomSheet(\n      shape: const BeveledRectangleBorder(),\n      isScrollControlled: true,\n      context: context,\n      builder: (context) {\n        return StatefulBuilder(\n          builder: (context, setModalState) {\n            return Padding(\n              padding: EdgeInsets.only(\n                bottom: MediaQuery.of(context).viewInsets.bottom,\n                left: 8,\n              ),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  Expanded(\n                    child: Container(\n                      constraints: const BoxConstraints(maxHeight: 34),\n                      child: TextField(\n                        style: const TextStyle(fontSize: 15),\n                        controller: textController,\n                        autofocus: true,\n                        textAlignVertical: TextAlignVertical.center,\n                        decoration: const InputDecoration(\n                          filled: true,\n                          floatingLabelBehavior: FloatingLabelBehavior.never,\n                          hintText: '发个友善的弹幕见证当下',\n                          hintStyle: TextStyle(fontSize: 14),\n                          alignLabelWithHint: true,\n                          contentPadding:\n                              EdgeInsets.symmetric(vertical: 8, horizontal: 12),\n                          border: OutlineInputBorder(\n                            borderSide: BorderSide.none,\n                            borderRadius: BorderRadius.all(Radius.circular(20)),\n                          ),\n                        ),\n                        onSubmitted: (msg) {\n                          showDanmakuDestinationPickerAndSend(msg);\n                          textController.clear();\n                          Navigator.pop(context);\n                        },\n                      ),\n                    ),\n                  ),\n                  IconButton(\n                    onPressed: () {\n                      final msg = textController.text;\n                      Navigator.pop(context);\n                      showDanmakuDestinationPickerAndSend(msg);\n                      textController.clear();\n                    },\n                    icon: Icon(\n                      Icons.send_rounded,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n                  )\n                ],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void showDanmakuDestinationPickerAndSend(String msg) async {\n    if (msg.trim().isEmpty) {\n      KazumiDialog.showToast(message: '弹幕内容为空');\n      return;\n    }\n\n    final DanmakuDestination? result =\n        await showModalBottomSheet<DanmakuDestination>(\n      context: context,\n      shape: const BeveledRectangleBorder(),\n      builder: (context) {\n        return SafeArea(\n          left: false,\n          right: false,\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              ListTile(\n                title: const Text('发送到聊天室'),\n                onTap: () =>\n                    Navigator.of(context).pop(DanmakuDestination.chatRoom),\n              ),\n              ListTile(\n                title: const Text('发送到远程弹幕库'),\n                onTap: () =>\n                    Navigator.of(context).pop(DanmakuDestination.remoteDanmaku),\n              ),\n              const SizedBox(height: 8),\n            ],\n          ),\n        );\n      },\n    );\n\n    if (result != null) {\n      setState(() {});\n      playerController.danmakuDestination = result;\n      sendDanmaku(msg);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final bool islandScape =\n        MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height;\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      openTabBodyAnimated();\n    });\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        if (didPop) {\n          return;\n        }\n        onBackPressed(context);\n      },\n      child: OrientationBuilder(builder: (context, orientation) {\n        if (!Utils.isDesktop()) {\n          if (orientation == Orientation.landscape &&\n              !videoPageController.isFullscreen) {\n            videoPageController.enterFullScreen();\n          } else if (orientation == Orientation.portrait &&\n              videoPageController.isFullscreen) {\n            videoPageController.exitFullScreen();\n            menuJumpToCurrentEpisode();\n            videoPageController.showTabBody = true;\n          }\n        }\n        return Observer(builder: (context) {\n          return Scaffold(\n            appBar: null,\n            body: SafeArea(\n                top: !videoPageController.isFullscreen,\n                // set iOS and Android navigation bar to immersive\n                bottom: false,\n                left: !videoPageController.isFullscreen,\n                right: !videoPageController.isFullscreen,\n                child: Stack(\n                  alignment: Alignment.centerRight,\n                  children: [\n                    Column(\n                      children: [\n                        Flexible(\n                          // make it unflexible when not wideScreen.\n                          flex: (islandScape) ? 1 : 0,\n                          child: Container(\n                            color: Colors.black,\n                            height: (islandScape)\n                                ? MediaQuery.sizeOf(context).height\n                                : MediaQuery.sizeOf(context).width * 9 / 16,\n                            width: MediaQuery.sizeOf(context).width,\n                            child: playerBody,\n                          ),\n                        ),\n                        // when not wideScreen, show tabBody on the bottom\n                        if (!islandScape) Expanded(child: tabBody),\n                      ],\n                    ),\n\n                    // when is wideScreen, show tabBody on the right side with SlideTransition or direct visibility\n                    if (islandScape && videoPageController.showTabBody) ...[\n                      if (disableAnimations) ...[\n                        sideTabMask,\n                        sideTabBody,\n                      ] else ...[\n                        FadeTransition(\n                          opacity: _maskOpacityAnimation,\n                          child: sideTabMask,\n                        ),\n                        SlideTransition(\n                          position: _rightOffsetAnimation,\n                          child: sideTabBody,\n                        ),\n                      ],\n                    ],\n                  ],\n                )),\n          );\n        });\n      }),\n    );\n  }\n\n  Widget get sideTabBody {\n    return SizedBox(\n      height: MediaQuery.sizeOf(context).height,\n      width: (!Utils.isDesktop() && !Utils.isTablet())\n          ? MediaQuery.sizeOf(context).height\n          : (MediaQuery.sizeOf(context).width / 3 > 420\n              ? 420\n              : MediaQuery.sizeOf(context).width / 3),\n      child: Container(\n        color: Theme.of(context).canvasColor,\n        child: GridViewObserver(\n          controller: observerController,\n          child: (Utils.isDesktop() || Utils.isTablet())\n              ? tabBody\n              : Column(\n                  children: [\n                    menuBar,\n                    menuBody,\n                  ],\n                ),\n        ),\n      ),\n    );\n  }\n\n  Widget get sideTabMask {\n    return GestureDetector(\n      onTap: closeTabBodyAnimated,\n      child: Container(\n        decoration: BoxDecoration(\n          gradient: LinearGradient(\n            begin: Alignment.centerLeft,\n            end: Alignment.centerRight,\n            colors: [\n              Colors.black.withValues(alpha: 0.5),\n              Colors.transparent,\n            ],\n          ),\n        ),\n        width: double.infinity,\n        height: double.infinity,\n      ),\n    );\n  }\n\n  Widget get playerBody {\n    return Stack(\n      children: [\n        Positioned.fill(\n          child: Stack(\n            children: [\n              if (videoPageController.loading ||\n                  playerController.loading ||\n                  videoPageController.errorMessage != null)\n                Container(\n                  color: Colors.black,\n                  child: Observer(builder: (context) {\n                    return Center(\n                      child: videoPageController.errorMessage != null\n                          ? Column(\n                              mainAxisAlignment: MainAxisAlignment.center,\n                              children: [\n                                Icon(Icons.error_outline,\n                                    color: Theme.of(context).colorScheme.error,\n                                    size: 48),\n                                const SizedBox(height: 16),\n                                Padding(\n                                  padding: const EdgeInsets.symmetric(\n                                      horizontal: 32),\n                                  child: Text(\n                                    videoPageController.errorMessage!,\n                                    style: const TextStyle(\n                                        color: Colors.white, fontSize: 16),\n                                    textAlign: TextAlign.center,\n                                  ),\n                                ),\n                              ],\n                            )\n                          : Column(\n                              mainAxisAlignment: MainAxisAlignment.center,\n                              children: [\n                                CircularProgressIndicator(\n                                    color: Theme.of(context)\n                                        .colorScheme\n                                        .tertiaryContainer),\n                                const SizedBox(height: 10),\n                                Text(\n                                  videoPageController.loading\n                                      ? '视频资源解析中'\n                                      : '视频资源解析成功, 播放器加载中',\n                                  style: const TextStyle(color: Colors.white),\n                                ),\n                              ],\n                            ),\n                    );\n                  }),\n                ),\n              Visibility(\n                visible:\n                    (videoPageController.loading || playerController.loading) &&\n                        showDebugLog,\n                child: Container(\n                  color: Colors.black,\n                  child: Align(\n                    alignment: Alignment.center,\n                    child: ListView.builder(\n                      shrinkWrap: true,\n                      itemCount: webviewLogLines.length,\n                      itemBuilder: (context, index) {\n                        return Text(\n                          webviewLogLines.isEmpty ? '' : webviewLogLines[index],\n                          style: const TextStyle(\n                            color: Colors.white,\n                          ),\n                          textAlign: TextAlign.center,\n                        );\n                      },\n                    ),\n                  ),\n                ),\n              ),\n              Stack(\n                children: [\n                  Positioned(\n                    top: 0,\n                    left: 0,\n                    right: 0,\n                    child: EmbeddedNativeControlArea(\n                      requireOffset: !videoPageController.isFullscreen,\n                      child: Row(\n                        children: [\n                          IconButton(\n                            icon: const Icon(Icons.arrow_back,\n                                color: Colors.white),\n                            onPressed: () => onBackPressed(context),\n                          ),\n                          const Expanded(\n                              child: dtb.DragToMoveArea(\n                                  child: SizedBox(height: 40))),\n                          IconButton(\n                            icon: const Icon(Icons.refresh_outlined,\n                                color: Colors.white),\n                            onPressed: () {\n                              changeEpisode(videoPageController.currentEpisode,\n                                  currentRoad: videoPageController.currentRoad);\n                            },\n                          ),\n                          Visibility(\n                            visible: MediaQuery.sizeOf(context).width >\n                                MediaQuery.sizeOf(context).height,\n                            child: IconButton(\n                              onPressed: () {\n                                videoPageController.showTabBody =\n                                    !videoPageController.showTabBody;\n                                openTabBodyAnimated();\n                              },\n                              icon: Icon(\n                                videoPageController.showTabBody\n                                    ? Icons.menu_open\n                                    : Icons.menu_open_outlined,\n                                color: Colors.white,\n                              ),\n                            ),\n                          ),\n                          IconButton(\n                            icon: Icon(\n                                showDebugLog\n                                    ? Icons.bug_report\n                                    : Icons.bug_report_outlined,\n                                color: Colors.white),\n                            onPressed: () {\n                              switchDebugConsole();\n                            },\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n        Positioned.fill(\n          child: playerController.loading\n              ? Container()\n              : PlayerItem(\n                  openMenu: openTabBodyAnimated,\n                  locateEpisode: menuJumpToCurrentEpisode,\n                  changeEpisode: changeEpisode,\n                  onBackPressed: onBackPressed,\n                  keyboardFocus: keyboardFocus,\n                  sendDanmaku: sendDanmaku,\n                  disableAnimations: disableAnimations,\n                  showDanmakuDestinationPickerAndSend:\n                      showDanmakuDestinationPickerAndSend,\n                  pauseForTimedShutdown: pauseForTimedShutdown,\n                ),\n        ),\n      ],\n    );\n  }\n\n  Widget get menuBar {\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          const Text(' 合集 '),\n          Expanded(\n            child: Text(\n              videoPageController.title,\n              overflow: TextOverflow.ellipsis,\n              style: TextStyle(\n                fontSize: 12,\n                color: Theme.of(context).colorScheme.outline,\n              ),\n            ),\n          ),\n          const SizedBox(width: 10),\n          MenuAnchor(\n            consumeOutsideTap: true,\n            builder: (_, MenuController controller, __) {\n              return SizedBox(\n                height: 34,\n                child: TextButton(\n                  style: ButtonStyle(\n                    padding: WidgetStateProperty.all(EdgeInsets.zero),\n                  ),\n                  onPressed: () {\n                    if (controller.isOpen) {\n                      controller.close();\n                    } else {\n                      controller.open();\n                    }\n                  },\n                  child: Text(\n                    '播放列表${currentRoad + 1} ',\n                    style: const TextStyle(fontSize: 13),\n                  ),\n                ),\n              );\n            },\n            menuChildren: List<MenuItemButton>.generate(\n              videoPageController.roadList.length,\n              (int i) => MenuItemButton(\n                onPressed: () {\n                  setState(() {\n                    currentRoad = i;\n                  });\n                },\n                child: Container(\n                  height: 48,\n                  constraints: BoxConstraints(minWidth: 112),\n                  child: Align(\n                    alignment: Alignment.centerLeft,\n                    child: Text(\n                      '播放列表${i + 1}',\n                      style: TextStyle(\n                        color: i == currentRoad\n                            ? Theme.of(context).colorScheme.primary\n                            : null,\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  DownloadEpisode? _getEpisodeFromRecords(\n      int episodeNumber, String episodePageUrl) {\n    final bangumiId = videoPageController.bangumiItem.id;\n    final pluginName = videoPageController.currentPlugin.name;\n\n    for (final record in downloadController.records) {\n      if (record.bangumiId == bangumiId && record.pluginName == pluginName) {\n        if (episodePageUrl.isNotEmpty) {\n          for (final episode in record.episodes.values) {\n            if (episode.episodePageUrl == episodePageUrl) {\n              return episode;\n            }\n          }\n        }\n        return record.episodes[episodeNumber];\n      }\n    }\n    return null;\n  }\n\n  Widget _buildDownloadStatusIcon(int episodeNumber, String episodePageUrl) {\n    // 离线模式下不显示下载状态图标\n    if (videoPageController.isOfflineMode) return const SizedBox.shrink();\n    final episode = _getEpisodeFromRecords(episodeNumber, episodePageUrl);\n    if (episode == null) return const SizedBox.shrink();\n    switch (episode.status) {\n      case DownloadStatus.completed:\n        return Icon(Icons.offline_pin,\n            size: 16, color: Theme.of(context).colorScheme.primary);\n      case DownloadStatus.downloading:\n        return SizedBox(\n          width: 16,\n          height: 16,\n          child: CircularProgressIndicator(\n            value: episode.progressPercent,\n            strokeWidth: 2,\n          ),\n        );\n      case DownloadStatus.failed:\n        return Icon(Icons.error_outline,\n            size: 16, color: Theme.of(context).colorScheme.error);\n      case DownloadStatus.paused:\n        return Icon(Icons.pause_circle_outline,\n            size: 16, color: Theme.of(context).colorScheme.outline);\n      case DownloadStatus.pending:\n      case DownloadStatus.resolving:\n        return SizedBox(\n          width: 16,\n          height: 16,\n          child: CircularProgressIndicator(strokeWidth: 2),\n        );\n      default:\n        return const SizedBox.shrink();\n    }\n  }\n\n  Widget get menuBody {\n    return Observer(\n      builder: (context) {\n        var cardList = <Widget>[];\n        for (var road in videoPageController.roadList) {\n          if (road.name == '播放列表${currentRoad + 1}') {\n            int count = 1;\n            for (var urlItem in road.data) {\n              int count0 = count;\n              cardList.add(Container(\n                margin: const EdgeInsets.only(bottom: 4),\n                child: Material(\n                  color: Theme.of(context).colorScheme.onInverseSurface,\n                  borderRadius: BorderRadius.circular(6),\n                  clipBehavior: Clip.hardEdge,\n                  child: InkWell(\n                    onTap: () async {\n                      if (count0 == videoPageController.currentEpisode &&\n                          videoPageController.currentRoad == currentRoad) {\n                        return;\n                      }\n                      KazumiLogger()\n                          .i('VideoPageController: video URL is $urlItem');\n                      closeTabBodyAnimated();\n                      changeEpisode(count0, currentRoad: currentRoad);\n                    },\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(\n                          vertical: 8, horizontal: 10),\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: <Widget>[\n                          Row(\n                            children: [\n                              if (count0 ==\n                                      (videoPageController.currentEpisode) &&\n                                  currentRoad ==\n                                      videoPageController\n                                          .currentRoad) ...<Widget>[\n                                Image.asset(\n                                  'assets/images/playing.gif',\n                                  color: Theme.of(context).colorScheme.primary,\n                                  height: 12,\n                                ),\n                                const SizedBox(width: 6)\n                              ],\n                              Expanded(\n                                  child: Text(\n                                road.identifier[count0 - 1],\n                                maxLines: 2,\n                                overflow: TextOverflow.ellipsis,\n                                style: TextStyle(\n                                    fontSize: 13,\n                                    color: (count0 ==\n                                                videoPageController\n                                                    .currentEpisode &&\n                                            currentRoad ==\n                                                videoPageController.currentRoad)\n                                        ? Theme.of(context).colorScheme.primary\n                                        : Theme.of(context)\n                                            .colorScheme\n                                            .onSurface),\n                              )),\n                              _buildDownloadStatusIcon(count0, urlItem),\n                              const SizedBox(width: 2),\n                            ],\n                          ),\n                          const SizedBox(height: 3),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              ));\n              count++;\n            }\n          }\n        }\n        return Expanded(\n          child: Padding(\n            padding: const EdgeInsets.only(top: 0, right: 8, left: 8),\n            child: GridView.builder(\n              scrollDirection: Axis.vertical,\n              controller: scrollController,\n              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n                crossAxisCount: 3,\n                crossAxisSpacing: 10,\n                mainAxisSpacing: 5,\n                mainAxisExtent: 70,\n              ),\n              itemCount: cardList.length,\n              itemBuilder: (context, index) {\n                return cardList[index];\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget get tabBody {\n    int episodeNum = 0;\n    episodeNum = Utils.extractEpisodeNumber(videoPageController\n        .roadList[videoPageController.currentRoad]\n        .identifier[videoPageController.currentEpisode - 1]);\n    if (episodeNum == 0 ||\n        (!videoPageController.isOfflineMode &&\n            episodeNum >\n                videoPageController\n                    .roadList[videoPageController.currentRoad]\n                    .identifier\n                    .length)) {\n      episodeNum = videoPageController.isOfflineMode\n          ? videoPageController.actualEpisodeNumber\n          : videoPageController.currentEpisode;\n    }\n\n    return Container(\n      color: Theme.of(context).canvasColor,\n      child: DefaultTabController(\n        length: 2,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                TabBar(\n                  controller: tabController,\n                  dividerHeight: 0,\n                  isScrollable: true,\n                  tabAlignment: TabAlignment.start,\n                  labelPadding:\n                      const EdgeInsetsDirectional.only(start: 30, end: 30),\n                  onTap: (index) {\n                    if (index == 0) {\n                      menuJumpToCurrentEpisode();\n                    }\n                  },\n                  tabs: const [\n                    Tab(text: '选集'),\n                    Tab(text: '评论'),\n                  ],\n                ),\n                if (MediaQuery.sizeOf(context).width <=\n                    MediaQuery.sizeOf(context).height) ...[\n                  const Spacer(),\n                  Container(\n                    decoration: BoxDecoration(\n                      borderRadius: BorderRadius.circular(25),\n                      border: Border.all(\n                        color: playerController.danmakuOn\n                            ? Theme.of(context).hintColor\n                            : Theme.of(context).disabledColor,\n                        width: 0.5,\n                      ),\n                    ),\n                    width: 120,\n                    height: 31,\n                    child: GestureDetector(\n                      onTap: () {\n                        if (playerController.danmakuOn &&\n                            !videoPageController.loading) {\n                          showMobileDanmakuInput();\n                        } else if (videoPageController.loading) {\n                          KazumiDialog.showToast(message: '请等待视频加载完成');\n                        } else {\n                          KazumiDialog.showToast(message: '请先打开弹幕');\n                        }\n                      },\n                      child: Row(\n                        children: [\n                          Text(\n                            playerController.danmakuOn\n                                ? '  点我发弹幕  '\n                                : '  已关闭弹幕  ',\n                            softWrap: false,\n                            overflow: TextOverflow.clip,\n                            style: TextStyle(\n                              color: playerController.danmakuOn\n                                  ? Theme.of(context).hintColor\n                                  : Theme.of(context).disabledColor,\n                            ),\n                          ),\n                          Icon(\n                            Icons.send_rounded,\n                            size: 20,\n                            color: playerController.danmakuOn\n                                ? Theme.of(context).hintColor\n                                : Theme.of(context).disabledColor,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ],\n                const SizedBox(width: 8),\n              ],\n            ),\n            Divider(height: Utils.isDesktop() ? 0.5 : 0.2),\n            Expanded(\n              child: TabBarView(\n                controller: tabController,\n                children: [\n                  Stack(\n                    children: [\n                      GridViewObserver(\n                        controller: observerController,\n                        child: Column(\n                          children: [\n                            menuBar,\n                            menuBody,\n                          ],\n                        ),\n                      ),\n                      if (!videoPageController.isOfflineMode)\n                        Positioned(\n                          right: 16,\n                          bottom: 16,\n                          child: FloatingActionButton(\n                            child: const Icon(Icons.download_rounded),\n                            onPressed: () {\n                              showModalBottomSheet(\n                                context: context,\n                                isScrollControlled: true,\n                                builder: (context) =>\n                                    DownloadEpisodeSheet(road: currentRoad),\n                              );\n                            },\n                          ),\n                        ),\n                    ],\n                  ),\n                  EpisodeInfo(\n                    episode: episodeNum,\n                    child: EpisodeCommentsSheet(),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/webdav_editor/webdav_editor_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:kazumi/utils/webdav.dart';\n\nclass WebDavEditorPage extends StatefulWidget {\n  const WebDavEditorPage({\n    super.key,\n  });\n\n  @override\n  State<WebDavEditorPage> createState() => _WebDavEditorPageState();\n}\n\nclass _WebDavEditorPageState extends State<WebDavEditorPage> {\n  final TextEditingController webDavURLController = TextEditingController();\n  final TextEditingController webDavUsernameController =\n      TextEditingController();\n  final TextEditingController webDavPasswordController =\n      TextEditingController();\n  Box setting = GStorage.setting;\n  bool passwordVisible = false;\n\n  @override\n  void initState() {\n    super.initState();\n    webDavURLController.text =\n        setting.get(SettingBoxKey.webDavURL, defaultValue: '');\n    webDavUsernameController.text =\n        setting.get(SettingBoxKey.webDavUsername, defaultValue: '');\n    webDavPasswordController.text =\n        setting.get(SettingBoxKey.webDavPassword, defaultValue: '');\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: const SysAppBar(\n        title: Text('WEBDAV编辑'),\n      ),\n      body: SingleChildScrollView(\n        padding: const EdgeInsets.all(16.0),\n        child: Center(\n          child: SizedBox(\n            width: (MediaQuery.of(context).size.width > 1000) ? 1000 : null,\n            child: Column(\n              children: [\n                TextField(\n                  controller: webDavURLController,\n                  decoration: const InputDecoration(\n                      labelText: 'URL', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: webDavUsernameController,\n                  decoration: const InputDecoration(\n                      labelText: 'Username', border: OutlineInputBorder()),\n                ),\n                const SizedBox(height: 20),\n                TextField(\n                  controller: webDavPasswordController,\n                  obscureText: !passwordVisible,\n                  decoration: InputDecoration(\n                    labelText: 'Password',\n                    border: const OutlineInputBorder(),\n                    suffixIcon: IconButton(\n                      onPressed: () {\n                        setState(() {\n                          passwordVisible = !passwordVisible;\n                        });\n                      },\n                      icon: Icon(passwordVisible\n                          ? Icons.visibility_rounded\n                          : Icons.visibility_off_rounded),\n                    ),\n                  ),\n                ),\n                // const SizedBox(height: 20),\n                // ExpansionTile(\n                //   title: const Text('高级选项'),\n                //   children: [],\n                // ),\n              ],\n            ),\n          ),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        child: const Icon(Icons.save),\n        onPressed: () async {\n          setting.put(SettingBoxKey.webDavURL, webDavURLController.text);\n          setting.put(\n              SettingBoxKey.webDavUsername, webDavUsernameController.text);\n          setting.put(\n              SettingBoxKey.webDavPassword, webDavPasswordController.text);\n          var webDav = WebDav();\n          try {\n            await webDav.init();\n          } catch (e) {\n            KazumiDialog.showToast(message: '配置失败 ${e.toString()}');\n            await setting.put(SettingBoxKey.webDavEnable, false);\n            return;\n          }\n          KazumiDialog.showToast(message: '配置成功, 开始测试');\n          try {\n            await webDav.ping();\n            KazumiDialog.showToast(message: '测试成功');\n          } catch (e) {\n            KazumiDialog.showToast(message: '测试失败 ${e.toString()}');\n            await setting.put(SettingBoxKey.webDavEnable, false);\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/webdav_editor/webdav_module.dart",
    "content": "import 'package:kazumi/pages/webdav_editor/webdav_editor_page.dart';\nimport 'package:kazumi/pages/webdav_editor/webdav_setting.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\n\nclass WebDavModule extends Module {\n  @override\n  void binds(i) {}\n\n  @override\n  void routes(r) {\n    r.child(\"/\", child: (_) => const WebDavSettingsPage());\n    r.child(\"/editor\",\n        child: (_) => const WebDavEditorPage(),);\n  }\n}\n"
  },
  {
    "path": "lib/pages/webdav_editor/webdav_setting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/webdav.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/bean/appbar/sys_app_bar.dart';\nimport 'package:card_settings_ui/card_settings_ui.dart';\n\nclass WebDavSettingsPage extends StatefulWidget {\n  const WebDavSettingsPage({super.key});\n\n  @override\n  State<WebDavSettingsPage> createState() => _PlayerSettingsPageState();\n}\n\nclass _PlayerSettingsPageState extends State<WebDavSettingsPage> {\n  Box setting = GStorage.setting;\n  late bool webDavEnable;\n  late bool webDavEnableHistory;\n  late bool enableGitProxy;\n\n  @override\n  void initState() {\n    super.initState();\n    webDavEnable = setting.get(SettingBoxKey.webDavEnable, defaultValue: false);\n    webDavEnableHistory =\n        setting.get(SettingBoxKey.webDavEnableHistory, defaultValue: false);\n    enableGitProxy =\n        setting.get(SettingBoxKey.enableGitProxy, defaultValue: false);\n  }\n\n  void onBackPressed(BuildContext context) {\n    if (KazumiDialog.observer.hasKazumiDialog) {\n      KazumiDialog.dismiss();\n      return;\n    }\n  }\n\n  Future<void> checkWebDav() async {\n    var webDavURL =\n        await setting.get(SettingBoxKey.webDavURL, defaultValue: '');\n    if (webDavURL == '') {\n      await setting.put(SettingBoxKey.webDavEnable, false);\n      KazumiDialog.showToast(message: '未找到有效的webdav配置');\n      return;\n    }\n    try {\n      KazumiDialog.showToast(message: '尝试从WebDav同步');\n      var webDav = WebDav();\n      await webDav.downloadAndPatchHistory();\n      KazumiDialog.showToast(message: '同步成功');\n    } catch (e) {\n      if (e.toString().contains('Error: Not Found')) {\n        KazumiDialog.showToast(message: '配置成功, 这是一个不存在已有同步文件的全新WebDav');\n      } else {\n        KazumiDialog.showToast(message: '同步失败 ${e.toString()}');\n      }\n    }\n  }\n\n  Future<void> updateWebdav() async {\n    var webDavEnable =\n        await setting.get(SettingBoxKey.webDavEnable, defaultValue: false);\n    if (webDavEnable) {\n      KazumiDialog.showToast(message: '尝试上传到WebDav');\n      var webDav = WebDav();\n      try {\n        await webDav.ping();\n        try {\n          await webDav.updateHistory();\n          KazumiDialog.showToast(message: '同步成功');\n        } catch (e) {\n          KazumiDialog.showToast(message: '同步失败 ${e.toString()}');\n        }\n      } catch (e) {\n        KazumiDialog.showToast(message: 'WebDAV连接失败');\n      }\n    } else {\n      KazumiDialog.showToast(message: '未开启WebDav同步或配置无效');\n    }\n  }\n\n  Future<void> downloadWebdav() async {\n    var webDavEnable =\n        await setting.get(SettingBoxKey.webDavEnable, defaultValue: false);\n    if (webDavEnable) {\n      KazumiDialog.showToast(message: '尝试从WebDav同步');\n      var webDav = WebDav();\n      try {\n        await webDav.ping();\n        try {\n          await webDav.downloadAndPatchHistory();\n          KazumiDialog.showToast(message: '同步成功');\n        } catch (e) {\n          KazumiDialog.showToast(message: '同步失败 ${e.toString()}');\n        }\n      } catch (e) {\n        KazumiDialog.showToast(message: 'WebDAV连接失败');\n      }\n    } else {\n      KazumiDialog.showToast(message: '未开启WebDav同步或配置无效');\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily;\n    return PopScope(\n      canPop: true,\n      onPopInvokedWithResult: (bool didPop, Object? result) {\n        onBackPressed(context);\n      },\n      child: Scaffold(\n        appBar: const SysAppBar(title: Text('同步设置')),\n        body: SettingsList(\n          maxWidth: 1000,\n          sections: [\n            SettingsSection(\n              title: Text('Github', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    enableGitProxy = value ?? !enableGitProxy;\n                    await setting.put(\n                        SettingBoxKey.enableGitProxy, enableGitProxy);\n                    setState(() {});\n                  },\n                  title: Text('Github镜像', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('使用镜像访问规则托管仓库', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: enableGitProxy,\n                ),\n              ],\n            ),\n            SettingsSection(\n              title: Text('WEBDAV', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    webDavEnable = value ?? !webDavEnable;\n                    if (!WebDav().initialized && webDavEnable) {\n                      try {\n                        await WebDav().init();\n                      } catch (e) {\n                        webDavEnable = false;\n                        KazumiDialog.showToast(message: 'WEBDAV初始化失败 $e');\n                      }\n                    }\n                    if (!webDavEnable) {\n                      webDavEnableHistory = false;\n                      await setting.put(\n                          SettingBoxKey.webDavEnableHistory, false);\n                    }\n                    await setting.put(SettingBoxKey.webDavEnable, webDavEnable);\n                    if (mounted) {\n                      setState(() {});\n                    }\n                  },\n                  title: Text('WEBDAV同步', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: webDavEnable,\n                ),\n                SettingsTile.switchTile(\n                  onToggle: (value) async {\n                    if (!webDavEnable) {\n                      KazumiDialog.showToast(message: '请先开启WEBDAV同步');\n                      return;\n                    }\n                    webDavEnableHistory = value ?? !webDavEnableHistory;\n                    await setting.put(\n                        SettingBoxKey.webDavEnableHistory, webDavEnableHistory);\n                    setState(() {});\n                  },\n                  title: Text('观看记录同步', style: TextStyle(fontFamily: fontFamily)),\n                  description: Text('允许自动同步观看记录', style: TextStyle(fontFamily: fontFamily)),\n                  initialValue: webDavEnableHistory,\n                ),\n                SettingsTile.navigation(\n                  onPressed: (_) async {\n                    Modular.to.pushNamed('/settings/webdav/editor');\n                  },\n                  title: Text('WEBDAV配置', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              bottomInfo: Text('立即上传观看记录到WEBDAV', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile(\n                  trailing: const Icon(Icons.cloud_upload_rounded),\n                  onPressed: (_) {\n                    updateWebdav();\n                  },\n                  title: Text('手动上传', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n            SettingsSection(\n              bottomInfo: Text('立即下载观看记录到本地', style: TextStyle(fontFamily: fontFamily)),\n              tiles: [\n                SettingsTile(\n                  trailing: const Icon(Icons.cloud_download_rounded),\n                  onPressed: (_) {\n                    downloadWebdav();\n                  },\n                  title: Text('手动下载', style: TextStyle(fontFamily: fontFamily)),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugins/anti_crawler_config.dart",
    "content": "/// 反反爬虫验证类型\n///\n/// - [imageCaptcha] (1): WebView 抓取验证码图片，引导用户手动输入后提交\n/// - [autoClickButton] (2): WebView 检测到验证按钮后自动点击，无需用户交互\n///\n/// 保留整数表示以便将来新增第三种及更多验证方式时向后兼容。\nclass CaptchaType {\n  static const int imageCaptcha = 1;\n  static const int autoClickButton = 2;\n}\n\n/// 反反爬虫配置\n///\n/// 当网站对搜索请求返回验证码时，使用 WebView 加载搜索页，\n/// 根据 [captchaType] 采用不同策略完成验证，之后保存 Cookie 用于后续请求。\nclass AntiCrawlerConfig {\n  /// 是否启用反反爬虫功能\n  bool enabled;\n\n  /// 验证类型，见 [CaptchaType] 中的常量\n  ///\n  /// - [CaptchaType.imageCaptcha] (1)：图片验证码，需要用户手动输入\n  /// - [CaptchaType.autoClickButton] (2)：自动点击验证按钮，无需用户交互\n  int captchaType;\n\n  /// 验证码图片元素的 XPath 选择器（仅 captchaType == 1 时使用）\n  /// 用于在 WebView 页面中定位验证码图片，通过 Canvas 抓取其像素\n  String captchaImage;\n\n  /// 验证码输入框元素的 XPath 选择器（仅 captchaType == 1 时使用）\n  /// 用于在 WebView 页面中定位供用户输入验证码的 input 元素\n  String captchaInput;\n\n  /// 验证按钮元素的 XPath 选择器\n  ///\n  /// - captchaType == 1：提交验证码的按钮，模拟点击提交\n  /// - captchaType == 2：目标验证按钮（如\"我不是机器人\"），检测到后自动点击\n  String captchaButton;\n\n  AntiCrawlerConfig({\n    required this.enabled,\n    required this.captchaType,\n    required this.captchaImage,\n    required this.captchaInput,\n    required this.captchaButton,\n  });\n\n  factory AntiCrawlerConfig.fromJson(Map<String, dynamic> json) {\n    return AntiCrawlerConfig(\n      enabled: json['enabled'] ?? false,\n      captchaType: json['captchaType'] ?? CaptchaType.imageCaptcha,\n      captchaImage: json['captchaImage'] ?? '',\n      captchaInput: json['captchaInput'] ?? '',\n      captchaButton: json['captchaButton'] ?? '',\n    );\n  }\n\n  factory AntiCrawlerConfig.empty() {\n    return AntiCrawlerConfig(\n      enabled: false,\n      captchaType: CaptchaType.imageCaptcha,\n      captchaImage: '',\n      captchaInput: '',\n      captchaButton: '',\n    );\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      'enabled': enabled,\n      'captchaType': captchaType,\n      'captchaImage': captchaImage,\n      'captchaInput': captchaInput,\n      'captchaButton': captchaButton,\n    };\n  }\n\n  AntiCrawlerConfig copyWith({\n    bool? enabled,\n    int? captchaType,\n    String? captchaImage,\n    String? captchaInput,\n    String? captchaButton,\n  }) {\n    return AntiCrawlerConfig(\n      enabled: enabled ?? this.enabled,\n      captchaType: captchaType ?? this.captchaType,\n      captchaImage: captchaImage ?? this.captchaImage,\n      captchaInput: captchaInput ?? this.captchaInput,\n      captchaButton: captchaButton ?? this.captchaButton,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugin_cookie_manager.dart",
    "content": "import 'package:cookie_jar/cookie_jar.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 每条规则的 Cookie 管理器\n///\n/// 为每条规则维护一个独立的内存 [CookieJar]，\n/// 通过 [saveFromWebView] 将 WebView 捕获的 document.cookie 字符串\n/// 解析后存入对应规则的 jar，用于后续 HTTP 请求的 CookieManager 拦截器。\n/// Cookie 仅在当前 App 会话内有效，重启后需重新验证。\nclass PluginCookieManager {\n  PluginCookieManager._();\n  static final PluginCookieManager instance = PluginCookieManager._();\n\n  final Map<String, CookieJar> _jars = {};\n\n  CookieJar getJar(String pluginName) {\n    return _jars.putIfAbsent(pluginName, () => CookieJar());\n  }\n\n  Future<void> saveFromWebView(\n      String pluginName, String pageUrl, String cookieString) async {\n    if (cookieString.trim().isEmpty) return;\n    final uri = Uri.tryParse(pageUrl);\n    if (uri == null) return;\n\n    final jar = getJar(pluginName);\n    final cookies = _parseCookieString(cookieString, uri);\n    if (cookies.isEmpty) return;\n\n    await jar.saveFromResponse(uri, cookies);\n    KazumiLogger().i(\n        '[PluginCookieManager] Saved ${cookies.length} cookies for $pluginName');\n  }\n\n  /// 解析字符串为 [Cookie] 列表\n  List<Cookie> _parseCookieString(String raw, Uri uri) {\n    final cookies = <Cookie>[];\n    for (final part in raw.split(';')) {\n      final trimmed = part.trim();\n      if (trimmed.isEmpty) continue;\n      final eqIndex = trimmed.indexOf('=');\n      if (eqIndex <= 0) continue;\n      final name = trimmed.substring(0, eqIndex).trim();\n      final value = trimmed.substring(eqIndex + 1).trim();\n      try {\n        final cookie = Cookie(name, value)\n          ..domain = uri.host\n          ..path = '/';\n        cookies.add(cookie);\n      } catch (_) {}\n    }\n    return cookies;\n  }\n\n  void clearCookies(String pluginName) {\n    _jars.remove(pluginName);\n  }\n\n  bool hasCookies(String pluginName) {\n    return _jars.containsKey(pluginName);\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugin_install_time_tracker.dart",
    "content": "// 记录规则安装时间\n// 使用文件修改时间作为规则的安装时间\nclass PluginInstallTimeTracker {\n  // 记录规则安装时间的映射\n  final Map<String, int> _installTimes = {};\n\n  // 设置规则的安装时间\n  void setInstallTime(String pluginName, int timestamp) {\n    _installTimes[pluginName] = timestamp;\n  }\n\n  // 获取规则的安装时间，如果不存在返回0\n  int getInstallTime(String pluginName) {\n    return _installTimes[pluginName] ?? 0;\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugin_validity_tracker.dart",
    "content": "// 记录规则有效性状态\n// 目前仅追踪搜索有效性：在本次启动后，规则是否成功返回过搜索结果\nclass PluginValidityTracker {\n  // 记录搜索有效的规则集合\n  final Set<String> _searchValidPlugins = {};\n\n  // 标记规则搜索有效（成功返回过搜索结果）\n  void markSearchValid(String pluginName) {\n    _searchValidPlugins.add(pluginName);\n  }\n\n  // 检查规则搜索是否有效（是否成功返回过搜索结果）\n  bool isSearchValid(String pluginName) {\n    return _searchValidPlugins.contains(pluginName);\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugins.dart",
    "content": "import 'package:dio/dio.dart';\nimport 'package:kazumi/modules/search/plugin_search_module.dart';\nimport 'package:kazumi/modules/roads/road_module.dart';\nimport 'package:kazumi/request/request.dart';\nimport 'package:html/parser.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/plugins/anti_crawler_config.dart';\nimport 'package:kazumi/plugins/plugin_cookie_manager.dart';\n\n/// Thrown by [Plugin.queryBangumi] when the response contains a CAPTCHA challenge\n/// (i.e. the [AntiCrawlerConfig.captchaImage] XPath selector matches something\n/// in the returned HTML).\nclass CaptchaRequiredException implements Exception {\n  final String pluginName;\n  const CaptchaRequiredException(this.pluginName);\n  @override\n  String toString() => 'CaptchaRequiredException: $pluginName requires captcha verification';\n}\n\n/// Thrown by [Plugin.queryBangumi] when the search request succeeds but the\n/// XPath selectors return no results.\nclass NoResultException implements Exception {\n  final String pluginName;\n  const NoResultException(this.pluginName);\n  @override\n  String toString() => 'NoResultException: $pluginName returned no search results';\n}\n\n/// Thrown by [Plugin.queryBangumi] when the HTTP request or HTML parsing\n/// fails for reasons other than a captcha challenge.\nclass SearchErrorException implements Exception {\n  final String pluginName;\n  final Object? cause;\n  const SearchErrorException(this.pluginName, {this.cause});\n  @override\n  String toString() => 'SearchErrorException: $pluginName search failed${cause != null ? ' ($cause)' : ''}';\n}\n\nclass Plugin {\n  String api;\n  String type;\n  String name;\n  String version;\n  bool muliSources;\n  bool useWebview;\n  /// Deprecated (always true)\n  bool useNativePlayer;\n  bool usePost;\n  bool useLegacyParser;\n  bool adBlocker;\n  String userAgent;\n  String baseUrl;\n  String searchURL;\n  String searchList;\n  String searchName;\n  String searchResult;\n  String chapterRoads;\n  String chapterResult;\n  String referer;\n  AntiCrawlerConfig antiCrawlerConfig;\n\n  Plugin({\n    required this.api,\n    required this.type,\n    required this.name,\n    required this.version,\n    required this.muliSources,\n    required this.useWebview,\n    required this.useNativePlayer,\n    required this.usePost,\n    required this.useLegacyParser,\n    required this.adBlocker,\n    required this.userAgent,\n    required this.baseUrl,\n    required this.searchURL,\n    required this.searchList,\n    required this.searchName,\n    required this.searchResult,\n    required this.chapterRoads,\n    required this.chapterResult,\n    required this.referer,\n    AntiCrawlerConfig? antiCrawlerConfig,\n  }) : antiCrawlerConfig = antiCrawlerConfig ?? AntiCrawlerConfig.empty();\n\n  factory Plugin.fromJson(Map<String, dynamic> json) {\n    return Plugin(\n        api: json['api'],\n        type: json['type'],\n        name: json['name'],\n        version: json['version'],\n        muliSources: json['muliSources'],\n        useWebview: json['useWebview'],\n        useNativePlayer: json['useNativePlayer'],\n        usePost: json['usePost'] ?? false,\n        useLegacyParser: json['useLegacyParser'] ?? false,\n        adBlocker: json['adBlocker'] ?? false,\n        userAgent: json['userAgent'],\n        baseUrl: json['baseURL'],\n        searchURL: json['searchURL'],\n        searchList: json['searchList'],\n        searchName: json['searchName'],\n        searchResult: json['searchResult'],\n        chapterRoads: json['chapterRoads'],\n        chapterResult: json['chapterResult'],\n        referer: json['referer'] ?? '',\n        antiCrawlerConfig: json['antiCrawlerConfig'] != null\n            ? AntiCrawlerConfig.fromJson(\n                Map<String, dynamic>.from(json['antiCrawlerConfig']))\n            : AntiCrawlerConfig.empty());\n  }\n\n  factory Plugin.fromTemplate() {\n    return Plugin(\n        api: Api.apiLevel.toString(),\n        type: 'anime',\n        name: '',\n        version: '',\n        muliSources: true,\n        useWebview: true,\n        useNativePlayer: true,\n        usePost: false,\n        useLegacyParser: false,\n        adBlocker: false,\n        userAgent: '',\n        baseUrl: '',\n        searchURL: '',\n        searchList: '',\n        searchName: '',\n        searchResult: '',\n        chapterRoads: '',\n        chapterResult: '',\n        referer: '',\n        antiCrawlerConfig: AntiCrawlerConfig.empty());\n  }\n\n  Map<String, dynamic> toJson() {\n    final Map<String, dynamic> data = <String, dynamic>{};\n    data['api'] = api;\n    data['type'] = type;\n    data['name'] = name;\n    data['version'] = version;\n    data['muliSources'] = muliSources;\n    data['useWebview'] = useWebview;\n    data['useNativePlayer'] = useNativePlayer;\n    data['usePost'] = usePost;\n    data['useLegacyParser'] = useLegacyParser;\n    data['adBlocker'] = adBlocker;\n    data['userAgent'] = userAgent;\n    data['baseURL'] = baseUrl;\n    data['searchURL'] = searchURL;\n    data['searchList'] = searchList;\n    data['searchName'] = searchName;\n    data['searchResult'] = searchResult;\n    data['chapterRoads'] = chapterRoads;\n    data['chapterResult'] = chapterResult;\n    data['referer'] = referer;\n    data['antiCrawlerConfig'] = antiCrawlerConfig.toJson();\n    return data;\n  }\n\n  Future<PluginSearchResponse> queryBangumi(String keyword,\n      {bool shouldRethrow = false}) async {\n    try {\n    String queryURL = searchURL.replaceAll('@keyword', keyword);\n    dynamic resp;\n    List<SearchItem> searchItems = [];\n    final String cookieHeader = await _cookieHeaderFor(queryURL);\n    if (usePost) {\n      Uri uri = Uri.parse(queryURL);\n      Map<String, String> queryParams = uri.queryParameters;\n      Uri postUri = Uri(\n        scheme: uri.scheme,\n        host: uri.host,\n        path: uri.path,\n      );\n      var httpHeaders = {\n        'referer': '$baseUrl/',\n        'Content-Type': 'application/x-www-form-urlencoded',\n        'Accept-Language': Utils.getRandomAcceptedLanguage(),\n        'Connection': 'keep-alive',\n        if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,\n      };\n      resp = await Request().post(postUri.toString(),\n          options: Options(headers: httpHeaders),\n          extra: {'customError': ''},\n          data: queryParams,\n          shouldRethrow: shouldRethrow);\n    } else {\n      var httpHeaders = {\n        'referer': '$baseUrl/',\n        'Accept-Language': Utils.getRandomAcceptedLanguage(),\n        'Connection': 'keep-alive',\n        if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,\n      };\n      resp = await Request().get(queryURL,\n          options: Options(headers: httpHeaders),\n          shouldRethrow: shouldRethrow,\n          extra: {'customError': ''});\n    }\n\n    var htmlString = resp.data.toString();\n    var htmlElement = parse(htmlString).documentElement!;\n\n    // Detect captcha challenge: if antiCrawlerConfig is enabled, check both\n    // captchaImage and captchaButton XPaths — if either matches, throw so\n    // callers can show the dedicated captcha UI instead of a generic error.\n    if (antiCrawlerConfig.enabled) {\n      final List<String> detectionXpaths = [\n        antiCrawlerConfig.captchaImage,\n        antiCrawlerConfig.captchaButton,\n      ].where((x) => x.isNotEmpty).toList();\n      final bool captchaDetected = detectionXpaths.any(\n          (xpath) => htmlElement.queryXPath(xpath).node != null);\n      if (captchaDetected) {\n        KazumiLogger().w('Plugin: $name detected captcha challenge in search response');\n        throw CaptchaRequiredException(name);\n      }\n    }\n\n    htmlElement.queryXPath(searchList).nodes.forEach((element) {\n      try {\n        SearchItem searchItem = SearchItem(\n          name: element.queryXPath(searchName).node!.text?.trim() ?? '',\n          src: element.queryXPath(searchResult).node!.attributes['href'] ?? '',\n        );\n        searchItems.add(searchItem);\n        KazumiLogger().i(\n            'Plugin: $name ${element.queryXPath(searchName).node!.text ?? ''} $baseUrl${element.queryXPath(searchResult).node!.attributes['href'] ?? ''}');\n      } catch (_) {}\n    });\n    if (searchItems.isEmpty) throw NoResultException(name);\n    return PluginSearchResponse(pluginName: name, data: searchItems);\n    } on CaptchaRequiredException {\n      rethrow;\n    } on NoResultException {\n      rethrow;\n    } catch (e, st) {\n      KazumiLogger().w('Plugin: $name search failed', error: e, stackTrace: st);\n      if (shouldRethrow) throw SearchErrorException(name, cause: e);\n      return PluginSearchResponse(pluginName: name, data: []);\n    }\n  }\n\n  Future<List<Road>> querychapterRoads(String url, {CancelToken? cancelToken}) async {\n    List<Road> roadList = [];\n    if (!url.contains('https')) {\n      url = url.replaceAll('http', 'https');\n    }\n    String queryURL = '';\n    if (url.contains(baseUrl)) {\n      queryURL = url;\n    } else {\n      queryURL = baseUrl + url;\n    }\n    var httpHeaders = {\n      'referer': '$baseUrl/',\n      'Accept-Language': Utils.getRandomAcceptedLanguage(),\n      'Connection': 'keep-alive',\n    };\n    try {\n      var resp =\n      await Request().get(queryURL, options: Options(headers: httpHeaders), cancelToken: cancelToken);\n      var htmlString = resp.data.toString();\n      var htmlElement = parse(htmlString).documentElement!;\n      int count = 1;\n      htmlElement.queryXPath(chapterRoads).nodes.forEach((element) {\n        try {\n          List<String> chapterUrlList = [];\n          List<String> chapterNameList = [];\n          element.queryXPath(chapterResult).nodes.forEach((item) {\n            String itemUrl = item.node.attributes['href'] ?? '';\n            String itemName = item.node.text ?? '';\n            chapterUrlList.add(itemUrl);\n            chapterNameList.add(itemName.replaceAll(RegExp(r'\\s+'), ''));\n          });\n          if (chapterUrlList.isNotEmpty && chapterNameList.isNotEmpty) {\n            Road road = Road(\n                name: '播放列表$count',\n                data: chapterUrlList,\n                identifier: chapterNameList);\n            roadList.add(road);\n            count++;\n          }\n        } catch (_) {}\n      });\n    } catch (_) {}\n    return roadList;\n  }\n\n  Future<String> testSearchRequest(String keyword,\n      {bool shouldRethrow = false, CancelToken? cancelToken}) async {\n    String queryURL = searchURL.replaceAll('@keyword', keyword);\n    dynamic resp;\n    if (usePost) {\n      Uri uri = Uri.parse(queryURL);\n      Map<String, String> queryParams = uri.queryParameters;\n      Uri postUri = Uri(\n        scheme: uri.scheme,\n        host: uri.host,\n        path: uri.path,\n      );\n      var httpHeaders = {\n        'referer': '$baseUrl/',\n        'Content-Type': 'application/x-www-form-urlencoded',\n        'Accept-Language': Utils.getRandomAcceptedLanguage(),\n        'Connection': 'keep-alive',\n      };\n      resp = await Request().post(postUri.toString(),\n          options: Options(headers: httpHeaders),\n          extra: {'customError': ''},\n          data: queryParams,\n          shouldRethrow: shouldRethrow,\n          cancelToken: cancelToken);\n    } else {\n      var httpHeaders = {\n        'referer': '$baseUrl/',\n        'Accept-Language': Utils.getRandomAcceptedLanguage(),\n        'Connection': 'keep-alive',\n      };\n      resp = await Request().get(queryURL,\n          options: Options(headers: httpHeaders),\n          shouldRethrow: shouldRethrow,\n          extra: {'customError': ''},\n          cancelToken: cancelToken);\n    }\n\n    return resp.data.toString();\n  }\n\n  Future<String> _cookieHeaderFor(String url) async {\n    if (!PluginCookieManager.instance.hasCookies(name)) return '';\n    final uri = Uri.tryParse(url);\n    if (uri == null) return '';\n    try {\n      final cookies =\n          await PluginCookieManager.instance.getJar(name).loadForRequest(uri);\n      if (cookies.isEmpty) return '';\n      return cookies.map((c) => '${c.name}=${c.value}').join('; ');\n    } catch (_) {\n      return '';\n    }\n  }\n\n  String buildFullUrl(String urlItem) {\n    if (urlItem.contains(baseUrl) ||\n        urlItem.contains(baseUrl.replaceAll('https', 'http'))) {\n      return urlItem;\n    }\n    return baseUrl + urlItem;\n  }\n\n  Map<String, String> buildHttpHeaders() {\n    return {\n      'user-agent': userAgent.isEmpty ? Utils.getRandomUA() : userAgent,\n      if (referer.isNotEmpty) 'referer': referer,\n    };\n  }\n\n  PluginSearchResponse testQueryBangumi(String htmlString) {\n    List<SearchItem> searchItems = [];\n    var htmlElement = parse(htmlString).documentElement!;\n    htmlElement.queryXPath(searchList).nodes.forEach((element) {\n      try {\n        SearchItem searchItem = SearchItem(\n          name: element.queryXPath(searchName).node!.text?.trim() ?? '',\n          src: element.queryXPath(searchResult).node!.attributes['href'] ?? '',\n        );\n        searchItems.add(searchItem);\n        KazumiLogger().i(\n            'Plugin: $name ${element.queryXPath(searchName).node!.text ?? ''} $baseUrl${element.queryXPath(searchResult).node!.attributes['href'] ?? ''}');\n      } catch (_) {}\n    });\n    PluginSearchResponse pluginSearchResponse =\n    PluginSearchResponse(pluginName: name, data: searchItems);\n    return pluginSearchResponse;\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugins_controller.dart",
    "content": "import 'dart:io';\nimport 'dart:convert';\nimport 'package:mobx/mobx.dart';\nimport 'package:flutter/services.dart' show rootBundle, AssetManifest;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/plugins/plugin_validity_tracker.dart';\nimport 'package:kazumi/plugins/plugin_install_time_tracker.dart';\nimport 'package:kazumi/request/plugin.dart';\nimport 'package:kazumi/modules/plugin/plugin_http_module.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/request/api.dart';\n\npart 'plugins_controller.g.dart';\n\n// 从 1.5.1 版本开始，规则文件储存在单一的 plugins.json 文件中。\n// 之前的版本中，规则以分离文件形式存储，版本更新后将这些分离文件合并为单一的 plugins.json 文件。\n\nclass PluginsController = _PluginsController with _$PluginsController;\n\nabstract class _PluginsController with Store {\n  @observable\n  ObservableList<Plugin> pluginList = ObservableList.of([]);\n\n  @observable\n  ObservableList<PluginHTTPItem> pluginHTTPList = ObservableList.of([]);\n\n  // 规则有效性追踪器\n  final validityTracker = PluginValidityTracker();\n\n  // 规则安装时间追踪器\n  final installTimeTracker = PluginInstallTimeTracker();\n\n  String pluginsFileName = \"plugins.json\";\n\n  Directory? oldPluginDirectory;\n\n  Directory? newPluginDirectory;\n\n  // Initializes the plugin directory and loads all plugins\n  Future<void> init() async {\n    final directory = await getApplicationSupportDirectory();\n    oldPluginDirectory = Directory('${directory.path}/plugins');\n    if (!await oldPluginDirectory!.exists()) {\n      await oldPluginDirectory!.create(recursive: true);\n    }\n    newPluginDirectory = Directory('${directory.path}/plugins/v2');\n    if (!await newPluginDirectory!.exists()) {\n      await newPluginDirectory!.create(recursive: true);\n    }\n    await loadAllPlugins();\n  }\n\n  // Loads all plugins from the directory, populates the plugin list, and saves to plugins.json if needed\n  Future<void> loadAllPlugins() async {\n    pluginList.clear();\n    KazumiLogger()\n        .i('Plugins Directory: ${newPluginDirectory!.path}');\n    if (await newPluginDirectory!.exists()) {\n      final pluginsFile = File('${newPluginDirectory!.path}/$pluginsFileName');\n      if (await pluginsFile.exists()) {\n        final jsonString = await pluginsFile.readAsString();\n        pluginList.addAll(getPluginListFromJson(jsonString));\n        KazumiLogger()\n            .i('Plugin: Current Plugin number: ${pluginList.length}');\n      } else {\n        // No plugins.json\n        var jsonFiles = await getPluginFiles();\n        for (var filePath in jsonFiles) {\n          final file = File(filePath);\n          final jsonString = await file.readAsString();\n          final data = jsonDecode(jsonString);\n          final plugin = Plugin.fromJson(data);\n          pluginList.add(plugin);\n          await file.delete(recursive: true);\n        }\n        savePlugins();\n      }\n    } else {\n      KazumiLogger().w('Plugin: plugin directory does not exist');\n    }\n  }\n\n  // Retrieves a list of JSON plugin file paths from the plugin directory\n  Future<List<String>> getPluginFiles() async {\n    if (await oldPluginDirectory!.exists()) {\n      final jsonFiles = oldPluginDirectory!\n          .listSync()\n          .where((file) => file.path.endsWith('.json') && file is File)\n          .map((file) => file.path)\n          .toList();\n      return jsonFiles;\n    } else {\n      return [];\n    }\n  }\n\n  // Copies plugin JSON files from the assets to the plugin directory\n  Future<void> copyPluginsToExternalDirectory() async {\n    final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n    final assets = assetManifest.listAssets();\n    final jsonFiles = assets.where((String asset) =>\n        asset.startsWith('assets/plugins/') && asset.endsWith('.json'));\n\n    for (var filePath in jsonFiles) {\n      final jsonString = await rootBundle.loadString(filePath);\n      final plugin = Plugin.fromJson(jsonDecode(jsonString));\n      pluginList.add(plugin);\n    }\n    await savePlugins();\n    KazumiLogger().i(\n        'Plugin: ${jsonFiles.length} plugin files copied to ${newPluginDirectory!.path}');\n  }\n\n  List<dynamic> pluginListToJson() {\n    final List<dynamic> json = [];\n    for (var plugin in pluginList) {\n      json.add(plugin.toJson());\n    }\n    return json;\n  }\n\n  // Converts a JSON string into a list of Plugin objects.\n  List<Plugin> getPluginListFromJson(String jsonString) {\n    List<dynamic> json = jsonDecode(jsonString);\n    List<Plugin> plugins = [];\n    for (var j in json) {\n      plugins.add(Plugin.fromJson(j));\n    }\n    return plugins;\n  }\n\n  Future<void> removePlugin(Plugin plugin) async {\n    pluginList.removeWhere((p) => p.name == plugin.name);\n    await savePlugins();\n  }\n\n  // Update or add plugin\n  void updatePlugin(Plugin plugin) {\n    bool flag = false;\n    for (int i = 0; i < pluginList.length; ++i) {\n      if (pluginList[i].name == plugin.name) {\n        pluginList.replaceRange(i, i + 1, [plugin]);\n        flag = true;\n        break;\n      }\n    }\n    if (!flag) {\n      pluginList.add(plugin);\n    }\n    savePlugins();\n  }\n\n  void onReorder(int oldIndex, int newIndex) {\n    if (oldIndex < newIndex) {\n      newIndex -= 1;\n    }\n    final plugin = pluginList.removeAt(oldIndex);\n    pluginList.insert(newIndex, plugin);\n    savePlugins();\n  }\n\n  Future<void> savePlugins() async {\n    final jsonData = jsonEncode(pluginListToJson());\n    final pluginsFile = File('${newPluginDirectory!.path}/$pluginsFileName');\n    await pluginsFile.writeAsString(jsonData);\n    KazumiLogger().i('Plugin: updated plugin file $pluginsFileName');\n  }\n\n  Future<void> queryPluginHTTPList() async {\n    pluginHTTPList.clear();\n    var pluginHTTPListRes = await PluginHTTP.getPluginList();\n    pluginHTTPList.addAll(pluginHTTPListRes);\n  }\n\n  Future<Plugin?> queryPluginHTTP(String name) async {\n    Plugin? plugin;\n    plugin = await PluginHTTP.getPlugin(name);\n    return plugin;\n  }\n\n  String pluginStatus(PluginHTTPItem pluginHTTPItem) {\n    String pluginStatus = 'install';\n    for (Plugin plugin in pluginList) {\n      if (pluginHTTPItem.name == plugin.name) {\n        if (pluginHTTPItem.version == plugin.version) {\n          pluginStatus = 'installed';\n        } else {\n          pluginStatus = 'update';\n        }\n        break;\n      }\n    }\n    return pluginStatus;\n  }\n\n  String pluginUpdateStatus(Plugin plugin) {\n    if (!pluginHTTPList.any((p) => p.name == plugin.name)) {\n      return \"nonexistent\";\n    }\n    PluginHTTPItem p = pluginHTTPList.firstWhere(\n      (p) => p.name == plugin.name,\n    );\n    return p.version == plugin.version ? \"latest\" : \"updatable\";\n  }\n\n  Future<int> tryUpdatePlugin(Plugin plugin) async {\n    return await tryUpdatePluginByName(plugin.name);\n  }\n\n  Future<int> tryUpdatePluginByName(String name) async {\n    var pluginHTTPItem = await queryPluginHTTP(name);\n    if (pluginHTTPItem != null) {\n      if (int.parse(pluginHTTPItem.api) > Api.apiLevel) {\n        return 1;\n      }\n      updatePlugin(pluginHTTPItem);\n      return 0;\n    }\n    return 2;\n  }\n\n  Future<int> tryUpdateAllPlugin() async {\n    int count = 0;\n    for (Plugin plugin in pluginList) {\n      if (pluginUpdateStatus(plugin) == 'updatable') {\n        if (await tryUpdatePlugin(plugin) == 0) {\n          count++;\n        }\n      }\n    }\n    return count;\n  }\n\n  void removePlugins(Set<String> pluginNames) {\n    for (int i = pluginList.length - 1; i >= 0; --i) {\n      var name = pluginList[i].name;\n      if (pluginNames.contains(name)) {\n        pluginList.removeAt(i);\n      }\n    }\n    savePlugins();\n  }\n}\n"
  },
  {
    "path": "lib/plugins/plugins_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'plugins_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$PluginsController on _PluginsController, Store {\n  late final _$pluginListAtom =\n      Atom(name: '_PluginsController.pluginList', context: context);\n\n  @override\n  ObservableList<Plugin> get pluginList {\n    _$pluginListAtom.reportRead();\n    return super.pluginList;\n  }\n\n  @override\n  set pluginList(ObservableList<Plugin> value) {\n    _$pluginListAtom.reportWrite(value, super.pluginList, () {\n      super.pluginList = value;\n    });\n  }\n\n  late final _$pluginHTTPListAtom =\n      Atom(name: '_PluginsController.pluginHTTPList', context: context);\n\n  @override\n  ObservableList<PluginHTTPItem> get pluginHTTPList {\n    _$pluginHTTPListAtom.reportRead();\n    return super.pluginHTTPList;\n  }\n\n  @override\n  set pluginHTTPList(ObservableList<PluginHTTPItem> value) {\n    _$pluginHTTPListAtom.reportWrite(value, super.pluginHTTPList, () {\n      super.pluginHTTPList = value;\n    });\n  }\n\n  @override\n  String toString() {\n    return '''\npluginList: ${pluginList},\npluginHTTPList: ${pluginHTTPList}\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/providers/captcha/captcha_provider.dart",
    "content": "import 'dart:async';\n\nimport 'package:kazumi/webview/captcha/captcha_webview_controller.dart';\nimport 'package:kazumi/plugins/plugin_cookie_manager.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 验证码解决 Provider\n///\n/// 支持两种独立的验证流程：\n///\n/// **类型1：图片验证码**（[loadForCaptcha] + [submitCaptcha]）\n///   1. 初始化 WebView\n///   2. 加载搜索页面，注入 JS 脚本监听验证码图片\n///   3. 通过 [onCaptchaImageUrl] 流将验证码图片 URL 暴露给 UI\n///   4. UI 将用户输入的验证码传给 [submitCaptcha]\n///   5. 页面通过 AJAX 提交验证码\n///   6. 验证码图片消失后，获取页面 Cookie 并保存到 [PluginCookieManager]\n///   7. UI 发起重新检索\n///\n/// **类型2：自动点击验证按钮**（[loadForButtonClick]）\n///   1. 初始化 WebView\n///   2. 加载搜索页面，注入 JS 脚本轮询验证按钮\n///   3. 检测到按钮后自动模拟点击\n///   4. 按钮消失时，获取页面 Cookie 并保存到 [PluginCookieManager]\n///   5. 通过 [onVerified] 回调通知 UI 发起重新检索\nclass CaptchaProvider {\n  CaptchaWebviewController? _controller;\n\n  final StreamController<String?> _captchaImageStreamController =\n      StreamController<String?>.broadcast();\n\n  Stream<String?> get onCaptchaImageUrl => _captchaImageStreamController.stream;\n\n  StreamSubscription? _imageFoundSub;\n  StreamSubscription? _disappearedSub;\n  StreamSubscription? _logSub;\n\n  bool _isInitialized = false;\n  bool _disposed = false;\n  String _pageUrl = '';\n\n  Future<void> _ensureInitialized() async {\n    if (_isInitialized || _disposed) return;\n    _controller = CaptchaWebviewControllerFactory.getController();\n    final initializedFuture = _controller!.onInitialized.first\n        .timeout(const Duration(seconds: 10), onTimeout: () => false);\n\n    await _controller!.init();\n    if (_disposed) return;\n    await initializedFuture;\n    if (_disposed) return;\n\n    _logSub?.cancel();\n    _logSub = _controller!.onLog.listen((msg) => KazumiLogger().d(msg));\n\n    _isInitialized = true;\n    KazumiLogger().i('[CaptchaProvider] WebView initialized');\n  }\n\n  /// 加载指定页面并开始监听验证码图片\n  ///\n  /// [url] 要加载的页面地址\n  /// [captchaXpath] 验证码图片元素的 XPath\n  /// [inputXpath] 可选，验证码输入框的 XPath。如果提供，会在检测验证码前先触发输入框的 focus 事件\n  Future<void> loadForCaptcha(String url, String captchaXpath, {String? inputXpath}) async {\n    _pageUrl = url;\n    await _ensureInitialized();\n    if (_disposed || _controller == null) return;\n\n    _imageFoundSub?.cancel();\n    _imageFoundSub = _controller!.onCaptchaImageFound.listen((src) {\n      KazumiLogger().i('[CaptchaProvider] Captcha image found: $src');\n      if (!_captchaImageStreamController.isClosed) {\n        _captchaImageStreamController.add(src);\n      }\n    });\n\n    await _controller!.loadPage(url, captchaXpath, inputXpath: inputXpath);\n    KazumiLogger().i('[CaptchaProvider] Page loading: $url');\n  }\n\n  /// 提交验证码\n  ///\n  /// [captchaCode] 用户输入的验证码文本\n  /// [inputXpath]  验证码输入框元素的 XPath\n  /// [buttonXpath] 验证提交按钮元素的 XPath\n  /// [pluginName] 规则名（用于保存 Cookie）\n  /// [onVerified] 验证成功后的回调\n  Future<void> submitCaptcha({\n    required String captchaCode,\n    required String inputXpath,\n    required String buttonXpath,\n    required String pluginName,\n    required void Function() onVerified,\n  }) async {\n    if (_controller == null) {\n      KazumiLogger().w('[CaptchaProvider] submitCaptcha called before init');\n      return;\n    }\n\n    KazumiLogger().i('[CaptchaProvider] Submitting captcha code via interact');\n\n    bool _handled = false;\n\n    Future<void> onDisappeared() async {\n      if (_handled) return;\n      _handled = true;\n      _disappearedSub?.cancel();\n      final cookieString = await _controller!.getCookieString(_pageUrl);\n      KazumiLogger().i('[CaptchaProvider] Captured cookies: $cookieString');\n      if (cookieString.isNotEmpty) {\n        await PluginCookieManager.instance\n            .saveFromWebView(pluginName, _pageUrl, cookieString);\n        KazumiLogger()\n            .i('[CaptchaProvider] Cookies saved for plugin: $pluginName');\n      }\n      await _controller!.unloadPage();\n      onVerified();\n    }\n    _disappearedSub?.cancel();\n    _disappearedSub = _controller!.onCaptchaDisappeared.listen((_) {\n      onDisappeared();\n    });\n    await _controller!.submitCaptchaInteract(captchaCode, inputXpath, buttonXpath);\n  }\n\n  /// 加载页面并自动点击验证按钮\n  ///\n  /// [url] 要加载的页面地址\n  /// [buttonXpath] 验证按钮元素的 XPath，检测到后自动点击\n  /// [pluginName] 规则名（用于保存 Cookie）\n  /// [onVerified] 按钮消失（验证通过）后的回调\n  Future<void> loadForButtonClick({\n    required String url,\n    required String buttonXpath,\n    required String pluginName,\n    required void Function() onVerified,\n  }) async {\n    _pageUrl = url;\n    await _ensureInitialized();\n    if (_disposed || _controller == null) return;\n\n    bool _handled = false;\n\n    Future<void> onDisappeared() async {\n      if (_handled) return;\n      _handled = true;\n      _disappearedSub?.cancel();\n      final cookieString = await _controller!.getCookieString(_pageUrl);\n      KazumiLogger().i('[CaptchaProvider] (type2) Captured cookies: $cookieString');\n      if (cookieString.isNotEmpty) {\n        await PluginCookieManager.instance\n            .saveFromWebView(pluginName, _pageUrl, cookieString);\n        KazumiLogger()\n            .i('[CaptchaProvider] (type2) Cookies saved for plugin: $pluginName');\n      }\n      await _controller!.unloadPage();\n      onVerified();\n    }\n\n    _disappearedSub?.cancel();\n    _disappearedSub = _controller!.onCaptchaDisappeared.listen((_) {\n      onDisappeared();\n    });\n\n    await _controller!.loadPageForButtonClick(url, buttonXpath);\n    KazumiLogger().i('[CaptchaProvider] (type2) Page loading for button click: $url');\n  }\n\n  Future<void> saveAndUnload(String pluginName) async {\n    _disappearedSub?.cancel();\n    _disappearedSub = null;\n    // Capture locally before any await so dispose() nulling _controller\n    // between two awaits cannot cause a force-unwrap crash.\n    final controller = _controller;\n    if (controller == null || _pageUrl.isEmpty) return;\n    final cookieString = await controller.getCookieString(_pageUrl);\n    KazumiLogger()\n        .i('[CaptchaProvider] Captured cookies on cancel: $cookieString');\n    if (cookieString.isNotEmpty) {\n      await PluginCookieManager.instance\n          .saveFromWebView(pluginName, _pageUrl, cookieString);\n      KazumiLogger()\n          .i('[CaptchaProvider] Cookies saved on cancel for plugin: $pluginName');\n    }\n    await controller.unloadPage();\n  }\n\n  Stream<String>? get onLog => _controller?.onLog;\n\n  void dispose() {\n    if (_disposed) return;\n    _disposed = true;\n    _imageFoundSub?.cancel();\n    _disappearedSub?.cancel();\n    _logSub?.cancel();\n    if (!_captchaImageStreamController.isClosed) {\n      _captchaImageStreamController.close();\n    }\n    _controller?.dispose();\n    _controller = null;\n    _isInitialized = false;\n    KazumiLogger().i('[CaptchaProvider] Disposed');\n  }\n}\n"
  },
  {
    "path": "lib/providers/video/providers.dart",
    "content": "/// Video Source Provider 模块\n///\n/// 提供视频源解析的抽象层，支持：\n/// - WebView 在线解析\n///\n/// 使用示例：\n/// ```dart\n/// final provider = WebViewVideoSourceProvider();\n/// try {\n///   final source = await provider.resolve(\n///     episodeUrl,\n///     useLegacyParser: false,\n///   );\n///   print('Video URL: ${source.url}');\n/// } on VideoSourceTimeoutException {\n///   print('解析超时');\n/// } finally {\n///   provider.dispose();\n/// }\n/// ```\nlibrary;\n\nexport 'video_source_provider.dart';\nexport 'webview_video_source_provider.dart';\n"
  },
  {
    "path": "lib/providers/video/video_source_provider.dart",
    "content": "import 'dart:async';\n\n/// 视频源类型\nenum VideoSourceType {\n  /// 在线解析（WebView）\n  online,\n  /// 本地缓存\n  cached,\n}\n\n/// 视频源解析结果\nclass VideoSource {\n  /// 视频 URL (M3U8/MP4/本地路径)\n  final String url;\n\n  /// 播放偏移量（秒）\n  final int offset;\n\n  /// 视频源类型\n  final VideoSourceType type;\n\n  const VideoSource({\n    required this.url,\n    required this.offset,\n    required this.type,\n  });\n\n  @override\n  String toString() => 'VideoSource(url: $url, offset: $offset, type: $type)';\n}\n\n/// 视频源未找到异常\nclass VideoSourceNotFoundException implements Exception {\n  final String message;\n  const VideoSourceNotFoundException([this.message = 'Video source not found']);\n\n  @override\n  String toString() => 'VideoSourceNotFoundException: $message';\n}\n\n/// 视频源解析超时异常\nclass VideoSourceTimeoutException implements Exception {\n  final Duration timeout;\n  const VideoSourceTimeoutException(this.timeout);\n\n  @override\n  String toString() => 'VideoSourceTimeoutException: Timed out after ${timeout.inSeconds}s';\n}\n\n/// 视频源解析取消异常\nclass VideoSourceCancelledException implements Exception {\n  const VideoSourceCancelledException();\n\n  @override\n  String toString() => 'VideoSourceCancelledException: Resolution was cancelled';\n}\n\n/// 视频源提供者接口\n///\n/// 抽象视频源的获取方式，支持多种实现：\n/// - WebView 解析（在线）\n/// - 本地缓存读取\n/// - 组合策略（优先缓存，回退 WebView）\nabstract class IVideoSourceProvider {\n  /// 解析视频源 URL\n  ///\n  /// [episodeUrl] 集数页面 URL\n  /// [useLegacyParser] 是否使用旧版解析器（iframe 监听）\n  /// [offset] 播放偏移量（秒）\n  /// [timeout] 解析超时时间\n  ///\n  /// 返回 [VideoSource] 包含解析后的视频 URL 和元数据\n  ///\n  /// 可能抛出：\n  /// - [VideoSourceNotFoundException] 未找到视频源\n  /// - [VideoSourceTimeoutException] 解析超时\n  /// - [VideoSourceCancelledException] 解析被取消\n  Future<VideoSource> resolve(\n    String episodeUrl, {\n    required bool useLegacyParser,\n    int offset = 0,\n    Duration timeout = const Duration(seconds: 30),\n  });\n\n  /// 取消当前正在进行的解析\n  ///\n  /// 调用后，正在进行的 [resolve] 会抛出 [VideoSourceCancelledException]\n  void cancel();\n\n  /// 释放资源\n  void dispose();\n}\n"
  },
  {
    "path": "lib/providers/video/webview_video_source_provider.dart",
    "content": "import 'dart:async';\n\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:kazumi/providers/video/video_source_provider.dart';\n\n/// WebView 视频源提供者\n///\n/// 使用 WebView 解析视频页面，提取视频源 URL。\n/// WebView 实例在 Provider 生命周期内复用，切换集数时调用 unloadPage 释放页面资源，\n/// 仅在 [dispose] 时才真正销毁 WebView。\nclass WebViewVideoSourceProvider implements IVideoSourceProvider {\n  VideoWebviewController? _webview;\n  StreamSubscription? _logSubscription;\n\n  /// 单个 Provider 实例不能实现并发解析，单个 Provider 实例只能持有一个 Webview\n  /// 但是 Provider 可以在正在进行的解析未完成时，取消该解析并开始新的解析\n  /// 通过递增 ID 标识最新请求，取消旧请求\n  int _resolveId = 0;\n\n  final StreamController<String> _logController =\n      StreamController<String>.broadcast();\n  Stream<String> get onLog => _logController.stream;\n\n  @override\n  Future<VideoSource> resolve(\n    String episodeUrl, {\n    required bool useLegacyParser,\n    int offset = 0,\n    Duration timeout = const Duration(seconds: 15),\n  }) async {\n    _resolveId++;\n    final currentResolveId = _resolveId;\n\n    if (_webview == null) {\n      _webview = VideoWebviewControllerFactory.getController();\n      await _webview!.init();\n\n      _logSubscription = _webview!.onLog.listen((log) {\n        if (!_logController.isClosed) {\n          _logController.add(log);\n        }\n      });\n    }\n\n    try {\n      await _webview!.loadUrl(\n        episodeUrl,\n        useLegacyParser,\n        offset: offset,\n      );\n\n      if (currentResolveId != _resolveId) {\n        throw const VideoSourceCancelledException();\n      }\n\n      final event = await _webview!.onVideoURLParser.first.timeout(\n        timeout,\n        onTimeout: () {\n          if (currentResolveId != _resolveId) {\n            throw const VideoSourceCancelledException();\n          }\n          throw VideoSourceTimeoutException(timeout);\n        },\n      );\n\n      if (currentResolveId != _resolveId) {\n        throw const VideoSourceCancelledException();\n      }\n\n      return VideoSource(\n        url: event.$1,\n        offset: event.$2,\n        type: VideoSourceType.online,\n      );\n    } catch (e) {\n      if (e is VideoSourceCancelledException) {\n        rethrow;\n      }\n      if (currentResolveId != _resolveId) {\n        throw const VideoSourceCancelledException();\n      }\n      rethrow;\n    } finally {\n      if (currentResolveId == _resolveId) {\n        await _webview?.unloadPage();\n      }\n    }\n  }\n\n  @override\n  void cancel() {\n    _resolveId++;\n  }\n\n  @override\n  void dispose() {\n    cancel();\n    _logSubscription?.cancel();\n    _logSubscription = null;\n    _logController.close();\n    _webview?.dispose();\n    _webview = null;\n  }\n}\n"
  },
  {
    "path": "lib/repositories/collect_crud_repository.dart",
    "content": "import 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:kazumi/modules/collect/collect_change_module.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 收藏CRUD数据访问接口\n///\n/// 提供收藏数据的增删改查操作\nabstract class ICollectCrudRepository {\n  /// 获取所有收藏\n  List<CollectedBangumi> getAllCollectibles();\n\n  /// 获取单个收藏\n  ///\n  /// [id] 番剧ID\n  /// 返回收藏对象，如果不存在返回null\n  CollectedBangumi? getCollectible(int id);\n\n  /// 获取收藏类型\n  ///\n  /// [id] 番剧ID\n  /// 返回收藏类型值，未收藏返回0\n  int getCollectType(int id);\n\n  /// 添加或更新收藏\n  ///\n  /// [bangumiItem] 番剧信息\n  /// [type] 收藏类型\n  Future<void> addCollectible(BangumiItem bangumiItem, int type);\n\n  /// 更新收藏的番剧信息\n  ///\n  /// [bangumiItem] 更新后的番剧信息\n  Future<void> updateCollectible(BangumiItem bangumiItem);\n\n  /// 删除收藏\n  ///\n  /// [id] 番剧ID\n  Future<void> deleteCollectible(int id);\n\n  /// 记录收藏变更（用于WebDAV同步）\n  ///\n  /// [change] 变更记录\n  Future<void> addCollectChange(CollectedBangumiChange change);\n\n  /// 获取旧版收藏列表（用于迁移）\n  List<BangumiItem> getFavorites();\n\n  /// 清空旧版收藏（迁移后）\n  Future<void> clearFavorites();\n}\n\n/// 收藏CRUD数据访问实现类\n///\n/// 基于Hive实现的收藏CRUD数据访问层\nclass CollectCrudRepository implements ICollectCrudRepository {\n  final _collectiblesBox = GStorage.collectibles;\n  final _collectChangesBox = GStorage.collectChanges;\n  final _favoritesBox = GStorage.favorites;\n\n  @override\n  List<CollectedBangumi> getAllCollectibles() {\n    try {\n      return _collectiblesBox.values.cast<CollectedBangumi>().toList();\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get all collectibles failed',\n        error: e,\n      );\n      return [];\n    }\n  }\n\n  @override\n  CollectedBangumi? getCollectible(int id) {\n    try {\n      return _collectiblesBox.get(id);\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get collectible failed. id=$id',\n        error: e,\n      );\n      return null;\n    }\n  }\n\n  @override\n  int getCollectType(int id) {\n    try {\n      final collectible = _collectiblesBox.get(id);\n      return collectible?.type ?? 0;\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get collect type failed. id=$id',\n        error: e,\n      );\n      return 0;\n    }\n  }\n\n  @override\n  Future<void> addCollectible(BangumiItem bangumiItem, int type) async {\n    try {\n      final collectedBangumi = CollectedBangumi(\n        bangumiItem,\n        DateTime.now(),\n        type,\n      );\n      await _collectiblesBox.put(bangumiItem.id, collectedBangumi);\n      await _collectiblesBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: add collectible failed. id=${bangumiItem.id}, type=$type',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  Future<void> updateCollectible(BangumiItem bangumiItem) async {\n    try {\n      final collectible = _collectiblesBox.get(bangumiItem.id);\n      if (collectible == null) {\n        KazumiLogger().i(\n          'GStorage: update collectible failed. collectible not found, id=${bangumiItem.id}',\n        );\n        return;\n      }\n      collectible.bangumiItem = bangumiItem;\n      await _collectiblesBox.put(bangumiItem.id, collectible);\n      await _collectiblesBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update collectible failed. id=${bangumiItem.id}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  Future<void> deleteCollectible(int id) async {\n    try {\n      await _collectiblesBox.delete(id);\n      await _collectiblesBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: delete collectible failed. id=$id',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  Future<void> addCollectChange(CollectedBangumiChange change) async {\n    try {\n      await _collectChangesBox.put(change.id, change);\n      await _collectChangesBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: record collect change failed. changeId=${change.id}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  List<BangumiItem> getFavorites() {\n    try {\n      return _favoritesBox.values.cast<BangumiItem>().toList();\n    } catch (e) {\n      KazumiLogger().i(\n        'GStorage: get favorites failed',\n        error: e,\n      );\n      return [];\n    }\n  }\n\n  @override\n  Future<void> clearFavorites() async {\n    try {\n      await _favoritesBox.clear();\n      await _favoritesBox.flush();\n    } catch (e) {\n      KazumiLogger().i(\n        'GStorage: clear favorites failed',\n        error: e,\n      );\n      rethrow;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/repositories/collect_repository.dart",
    "content": "import 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/modules/collect/collect_type.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 收藏数据访问接口\n///\n/// 提供收藏相关的数据访问抽象，解耦业务逻辑与数据存储\nabstract class ICollectRepository {\n  /// 根据收藏类型获取番剧ID集合\n  ///\n  /// [type] 收藏类型\n  /// 返回符合条件的番剧ID集合\n  Set<int> getBangumiIdsByType(CollectType type);\n\n  /// 批量获取多种类型的番剧ID集合\n  ///\n  /// [types] 收藏类型列表\n  /// 返回符合条件的番剧ID集合（并集）\n  Set<int> getBangumiIdsByTypes(List<CollectType> types);\n\n  // ========== 搜索页过滤器设置 ==========\n\n  /// 获取搜索页\"不显示已看过番剧\"设置\n  bool getSearchNotShowWatchedBangumis();\n\n  /// 更新搜索页\"不显示已看过番剧\"设置\n  Future<void> updateSearchNotShowWatchedBangumis(bool value);\n\n  /// 获取搜索页\"不显示已抛弃番剧\"设置\n  bool getSearchNotShowAbandonedBangumis();\n\n  /// 更新搜索页\"不显示已抛弃番剧\"设置\n  Future<void> updateSearchNotShowAbandonedBangumis(bool value);\n\n  // ========== 时间表页过滤器设置 ==========\n\n  /// 获取时间表页\"不显示已抛弃番剧\"设置\n  bool getTimelineNotShowAbandonedBangumis();\n\n  /// 更新时间表页\"不显示已抛弃番剧\"设置\n  Future<void> updateTimelineNotShowAbandonedBangumis(bool value);\n\n  /// 获取时间表页\"不显示已看过番剧\"设置\n  bool getTimelineNotShowWatchedBangumis();\n\n  /// 更新时间表页\"不显示已看过番剧\"设置\n  Future<void> updateTimelineNotShowWatchedBangumis(bool value);\n\n  // ========== 其他设置 ==========\n\n  /// 获取隐私模式设置\n  bool getPrivateMode();\n}\n\n/// 收藏数据访问实现类\n///\n/// 基于Hive实现的收藏数据访问层\nclass CollectRepository implements ICollectRepository {\n  final _collectiblesBox = GStorage.collectibles;\n  final _settingBox = GStorage.setting;\n\n  @override\n  Set<int> getBangumiIdsByType(CollectType type) {\n    try {\n      return _collectiblesBox.values\n          .where((item) => item.type == type.value)\n          .map<int>((item) => item.bangumiItem.id)\n          .toSet();\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get bangumi IDs by type failed. type=${type.label}',\n        error: e,\n      );\n      return <int>{};\n    }\n  }\n\n  @override\n  Set<int> getBangumiIdsByTypes(List<CollectType> types) {\n    try {\n      final typeValues = types.map((t) => t.value).toSet();\n      return _collectiblesBox.values\n          .where((item) => typeValues.contains(item.type))\n          .map<int>((item) => item.bangumiItem.id)\n          .toSet();\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get bangumi IDs by types failed. types=${types.map((t) => t.label).join(\", \")}',\n        error: e,\n      );\n      return <int>{};\n    }\n  }\n\n  // ========== 搜索页过滤器设置实现 ==========\n\n  @override\n  bool getSearchNotShowWatchedBangumis() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.searchNotShowWatchedBangumis,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e) {\n      KazumiLogger().w(\n        'GStorage: get search not show watched bangumis setting failed, using default false',\n        error: e,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> updateSearchNotShowWatchedBangumis(bool value) async {\n    try {\n      await _settingBox.put(SettingBoxKey.searchNotShowWatchedBangumis, value);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update search not show watched bangumis setting failed. value=$value',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  bool getSearchNotShowAbandonedBangumis() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.searchNotShowAbandonedBangumis,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get search not show abandoned bangumis setting failed, using default false',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> updateSearchNotShowAbandonedBangumis(bool value) async {\n    try {\n      await _settingBox.put(SettingBoxKey.searchNotShowAbandonedBangumis, value);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update search not show abandoned bangumis setting failed. value=$value',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  // ========== 时间表页过滤器设置实现 ==========\n\n  @override\n  bool getTimelineNotShowAbandonedBangumis() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.timelineNotShowAbandonedBangumis,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get timeline not show abandoned bangumis setting failed, using default false',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> updateTimelineNotShowAbandonedBangumis(bool value) async {\n    try {\n      await _settingBox.put(SettingBoxKey.timelineNotShowAbandonedBangumis, value);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update timeline not show abandoned bangumis setting failed. value=$value',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  bool getTimelineNotShowWatchedBangumis() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.timelineNotShowWatchedBangumis,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get timeline not show watched bangumis setting failed, using default false',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> updateTimelineNotShowWatchedBangumis(bool value) async {\n    try {\n      await _settingBox.put(SettingBoxKey.timelineNotShowWatchedBangumis, value);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update timeline not show watched bangumis setting failed. value=$value',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  // ========== 其他设置实现 ==========\n\n  @override\n  bool getPrivateMode() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.privateMode,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get private mode setting failed, using default false',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/repositories/download_repository.dart",
    "content": "import 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nabstract class IDownloadRepository {\n  List<DownloadRecord> getAllRecords();\n  DownloadRecord? getRecord(String key);\n  Future<void> putRecord(DownloadRecord record);\n  Future<void> deleteRecord(String key);\n  Future<void> updateEpisode(String recordKey, int episodeNumber, DownloadEpisode episode);\n  Future<void> deleteEpisode(String recordKey, int episodeNumber);\n  bool getForceAdBlocker();\n\n  /// 获取指定番剧的下载记录\n  ///\n  /// [bangumiId] 番剧 ID\n  /// [pluginName] 插件名称\n  DownloadRecord? getRecordByBangumiId(int bangumiId, String pluginName);\n\n  /// 获取指定集数的下载信息\n  ///\n  /// [bangumiId] 番剧 ID\n  /// [pluginName] 插件名称\n  /// [episodeNumber] 集数编号\n  DownloadEpisode? getEpisode(int bangumiId, String pluginName, int episodeNumber);\n\n  /// 获取已完成下载的集数列表\n  ///\n  /// [bangumiId] 番剧 ID\n  /// [pluginName] 插件名称\n  /// 返回所有已完成下载的集数\n  List<DownloadEpisode> getCompletedEpisodes(int bangumiId, String pluginName);\n\n  /// 通过集数页面 URL 查找下载记录\n  ///\n  /// [bangumiId] 番剧 ID\n  /// [pluginName] 插件名称\n  /// [episodePageUrl] 集数页面 URL\n  /// 当 URL 为空时返回 null（兼容旧数据）\n  DownloadEpisode? getEpisodeByUrl(int bangumiId, String pluginName, String episodePageUrl);\n}\n\nclass DownloadRepository implements IDownloadRepository {\n  final _downloadsBox = GStorage.downloads;\n\n  @override\n  List<DownloadRecord> getAllRecords() {\n    final List<DownloadRecord> result = [];\n    try {\n      for (final key in _downloadsBox.keys) {\n        try {\n          final record = _downloadsBox.get(key);\n          if (record != null) {\n            // Merge in-memory progress into the record\n            final cachedEpisodes = _progressCache[key as String];\n            if (cachedEpisodes != null) {\n              for (final entry in cachedEpisodes.entries) {\n                record.episodes[entry.key] = entry.value;\n              }\n            }\n            result.add(record);\n          }\n        } catch (e) {\n          // 单条记录读取失败，跳过该记录并记录日志\n          KazumiLogger().w('DownloadRepository: failed to read record key=$key, skipping', error: e);\n        }\n      }\n    } catch (e) {\n      KazumiLogger().w('DownloadRepository: get all records failed', error: e);\n    }\n    return result;\n  }\n\n  @override\n  DownloadRecord? getRecord(String key) {\n    try {\n      final record = _downloadsBox.get(key);\n      if (record != null) {\n        // Merge in-memory progress into the record\n        final cachedEpisodes = _progressCache[key];\n        if (cachedEpisodes != null) {\n          for (final entry in cachedEpisodes.entries) {\n            record.episodes[entry.key] = entry.value;\n          }\n        }\n      }\n      return record;\n    } catch (e) {\n      KazumiLogger().w('DownloadRepository: get record failed. key=$key', error: e);\n      return null;\n    }\n  }\n\n  @override\n  Future<void> putRecord(DownloadRecord record) async {\n    try {\n      await _downloadsBox.put(record.key, record);\n      await _downloadsBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'DownloadRepository: put record failed. key=${record.key}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  Future<void> deleteRecord(String key) async {\n    try {\n      await _downloadsBox.delete(key);\n      await _downloadsBox.flush();\n      _progressCache.remove(key);\n      _lastPersistedStatus.removeWhere((k, v) => k.startsWith('${key}_'));\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'DownloadRepository: delete record failed. key=$key',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  /// Track last persisted status to avoid unnecessary writes\n  final Map<String, int> _lastPersistedStatus = {};\n\n  /// In-memory cache for progress updates (not persisted until status changes)\n  final Map<String, Map<int, DownloadEpisode>> _progressCache = {};\n\n  @override\n  Future<void> updateEpisode(String recordKey, int episodeNumber, DownloadEpisode episode) async {\n    try {\n      // Update in-memory cache\n      _progressCache.putIfAbsent(recordKey, () => {});\n      _progressCache[recordKey]![episodeNumber] = episode;\n\n      // Only persist to Hive when status changes (not on every progress update)\n      // This dramatically reduces disk I/O and prevents corruption on crash\n      final statusKey = '${recordKey}_$episodeNumber';\n      final lastStatus = _lastPersistedStatus[statusKey];\n      final shouldPersist = lastStatus != episode.status;\n\n      if (shouldPersist) {\n        final record = _downloadsBox.get(recordKey);\n        if (record == null) return;\n        record.episodes[episodeNumber] = episode;\n        await _downloadsBox.put(recordKey, record);\n        await _downloadsBox.flush();\n        _lastPersistedStatus[statusKey] = episode.status;\n      }\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'DownloadRepository: update episode failed. key=$recordKey, ep=$episodeNumber',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  /// Get episode with in-memory progress if available\n  DownloadEpisode? getEpisodeWithProgress(String recordKey, int episodeNumber) {\n    // Check in-memory cache first\n    final cached = _progressCache[recordKey]?[episodeNumber];\n    if (cached != null) return cached;\n\n    // Fall back to Hive\n    final record = getRecord(recordKey);\n    return record?.episodes[episodeNumber];\n  }\n\n  @override\n  bool getForceAdBlocker() {\n    return GStorage.setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false);\n  }\n\n  @override\n  Future<void> deleteEpisode(String recordKey, int episodeNumber) async {\n    try {\n      final record = _downloadsBox.get(recordKey);\n      if (record == null) return;\n      record.episodes.remove(episodeNumber);\n      if (record.episodes.isEmpty) {\n        await _downloadsBox.delete(recordKey);\n        _progressCache.remove(recordKey);\n        _lastPersistedStatus.removeWhere((k, v) => k.startsWith('${recordKey}_'));\n      } else {\n        await _downloadsBox.put(recordKey, record);\n        _progressCache[recordKey]?.remove(episodeNumber);\n        _lastPersistedStatus.remove('${recordKey}_$episodeNumber');\n      }\n      await _downloadsBox.flush();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'DownloadRepository: delete episode failed. key=$recordKey, ep=$episodeNumber',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      rethrow;\n    }\n  }\n\n  @override\n  DownloadRecord? getRecordByBangumiId(int bangumiId, String pluginName) {\n    final key = '${pluginName}_$bangumiId';\n    return getRecord(key);\n  }\n\n  @override\n  DownloadEpisode? getEpisode(int bangumiId, String pluginName, int episodeNumber) {\n    final record = getRecordByBangumiId(bangumiId, pluginName);\n    return record?.episodes[episodeNumber];\n  }\n\n  @override\n  List<DownloadEpisode> getCompletedEpisodes(int bangumiId, String pluginName) {\n    final record = getRecordByBangumiId(bangumiId, pluginName);\n    if (record == null) return [];\n\n    return record.episodes.values\n        .where((e) => e.status == DownloadStatus.completed)\n        .toList()\n      ..sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber));\n  }\n\n  @override\n  DownloadEpisode? getEpisodeByUrl(int bangumiId, String pluginName, String episodePageUrl) {\n    if (episodePageUrl.isEmpty) return null;\n    final record = getRecordByBangumiId(bangumiId, pluginName);\n    if (record == null) return null;\n    for (final episode in record.episodes.values) {\n      if (episode.episodePageUrl == episodePageUrl) {\n        return episode;\n      }\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/repositories/history_repository.dart",
    "content": "import 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/history/history_module.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 历史记录数据访问接口\n///\n/// 提供观看历史相关的数据访问抽象\nabstract class IHistoryRepository {\n  /// 获取所有历史记录（按时间倒序）\n  List<History> getAllHistories();\n\n  /// 获取特定番剧的历史记录\n  ///\n  /// [adapterName] 适配器名称\n  /// [bangumiItem] 番剧信息\n  /// 返回历史记录，不存在返回null\n  History? getHistory(String adapterName, BangumiItem bangumiItem);\n\n  /// 更新或创建历史记录\n  ///\n  /// [episode] 集数\n  /// [road] 线路\n  /// [adapterName] 适配器名称\n  /// [bangumiItem] 番剧信息\n  /// [progress] 观看进度\n  /// [lastSrc] 最后观看源\n  /// [lastWatchEpisodeName] 最后观看集名称\n  Future<void> updateHistory({\n    required int episode,\n    required int road,\n    required String adapterName,\n    required BangumiItem bangumiItem,\n    required Duration progress,\n    required String lastSrc,\n    required String lastWatchEpisodeName,\n  });\n\n  /// 获取上次观看的进度\n  ///\n  /// [bangumiItem] 番剧信息\n  /// [adapterName] 适配器名称\n  /// 返回观看进度，不存在返回null\n  Progress? getLastWatchingProgress(BangumiItem bangumiItem, String adapterName);\n\n  /// 查找特定集数的观看进度\n  ///\n  /// [bangumiItem] 番剧信息\n  /// [adapterName] 适配器名称\n  /// [episode] 集数\n  /// 返回观看进度，不存在返回null\n  Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode);\n\n  /// 删除历史记录\n  ///\n  /// [history] 要删除的历史记录\n  Future<void> deleteHistory(History history);\n\n  /// 清空特定集数的观看进度\n  ///\n  /// [bangumiItem] 番剧信息\n  /// [adapterName] 适配器名称\n  /// [episode] 集数\n  Future<void> clearProgress(BangumiItem bangumiItem, String adapterName, int episode);\n\n  /// 清空所有历史记录\n  Future<void> clearAllHistories();\n\n  /// 获取隐私模式设置\n  bool getPrivateMode();\n}\n\n/// 历史记录数据访问实现类\n///\n/// 基于Hive实现的历史记录数据访问层\nclass HistoryRepository implements IHistoryRepository {\n  final _historiesBox = GStorage.histories;\n  final _settingBox = GStorage.setting;\n\n  @override\n  List<History> getAllHistories() {\n    try {\n      var histories = _historiesBox.values.toList();\n      histories.sort(\n        (a, b) =>\n            b.lastWatchTime.millisecondsSinceEpoch -\n            a.lastWatchTime.millisecondsSinceEpoch,\n      );\n      return histories;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get all histories failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return [];\n    }\n  }\n\n  @override\n  History? getHistory(String adapterName, BangumiItem bangumiItem) {\n    try {\n      return _historiesBox.get(History.getKey(adapterName, bangumiItem));\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get history failed. bangumi=${bangumiItem.name}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return null;\n    }\n  }\n\n  @override\n  Future<void> updateHistory({\n    required int episode,\n    required int road,\n    required String adapterName,\n    required BangumiItem bangumiItem,\n    required Duration progress,\n    required String lastSrc,\n    required String lastWatchEpisodeName,\n  }) async {\n    try {\n      // 检查隐私模式\n      if (getPrivateMode()) {\n        return;\n      }\n\n      // 获取或创建历史记录\n      var history = _historiesBox.get(History.getKey(adapterName, bangumiItem)) ??\n          History(bangumiItem, episode, adapterName, DateTime.now(), lastSrc, lastWatchEpisodeName);\n\n      // 更新历史记录\n      history.lastWatchEpisode = episode;\n      history.lastWatchTime = DateTime.now();\n      if (lastSrc.isNotEmpty) {\n        history.lastSrc = lastSrc;\n      }\n      if (lastWatchEpisodeName.isNotEmpty) {\n        history.lastWatchEpisodeName = lastWatchEpisodeName;\n      }\n\n      // 更新观看进度\n      var prog = history.progresses[episode];\n      if (prog == null) {\n        history.progresses[episode] =\n            Progress(episode, road, progress.inMilliseconds);\n      } else {\n        prog.progress = progress;\n      }\n\n      // 保存到存储\n      await _historiesBox.put(history.key, history);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: update history failed. bangumi=${bangumiItem.name}, episode=$episode',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  Progress? getLastWatchingProgress(BangumiItem bangumiItem, String adapterName) {\n    try {\n      var history = _historiesBox.get(History.getKey(adapterName, bangumiItem));\n      return history?.progresses[history.lastWatchEpisode];\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get last watching progress failed. bangumi=${bangumiItem.name}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return null;\n    }\n  }\n\n  @override\n  Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode) {\n    try {\n      var history = _historiesBox.get(History.getKey(adapterName, bangumiItem));\n      return history?.progresses[episode];\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: find progress failed. bangumi=${bangumiItem.name}, episode=$episode',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return null;\n    }\n  }\n\n  @override\n  Future<void> deleteHistory(History history) async {\n    try {\n      await _historiesBox.delete(history.key);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: delete history failed. bangumi=${history.bangumiItem.name}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  Future<void> clearProgress(BangumiItem bangumiItem, String adapterName, int episode) async {\n    try {\n      var history = _historiesBox.get(History.getKey(adapterName, bangumiItem));\n      if (history != null && history.progresses[episode] != null) {\n        history.progresses[episode]!.progress = Duration.zero;\n        await _historiesBox.put(history.key, history);\n      }\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: clear progress failed. bangumi=${bangumiItem.name}, episode=$episode',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  Future<void> clearAllHistories() async {\n    try {\n      await _historiesBox.clear();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: clear all histories failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  bool getPrivateMode() {\n    try {\n      final value = _settingBox.get(\n        SettingBoxKey.privateMode,\n        defaultValue: false,\n      );\n      return value is bool ? value : false;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get private mode setting failed, using default false',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/repositories/search_history_repository.dart",
    "content": "import 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/modules/search/search_history_module.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// 搜索历史数据访问接口\n///\n/// 提供搜索历史相关的数据访问抽象\nabstract class ISearchHistoryRepository {\n  /// 获取所有搜索历史（按时间戳倒序）\n  List<SearchHistory> getAllHistories();\n\n  /// 保存搜索历史\n  ///\n  /// [keyword] 搜索关键词\n  /// 返回是否成功\n  Future<bool> saveHistory(String keyword);\n\n  /// 删除指定搜索历史\n  ///\n  /// [history] 要删除的历史记录\n  Future<void> deleteHistory(SearchHistory history);\n\n  /// 清空所有搜索历史\n  Future<void> clearAllHistories();\n\n  /// 删除重复的历史记录\n  ///\n  /// [keyword] 关键词\n  Future<void> deleteDuplicates(String keyword);\n\n  /// 检查是否达到最大历史记录数\n  ///\n  /// [maxCount] 最大记录数\n  /// 返回是否已满\n  bool isHistoryFull(int maxCount);\n\n  /// 删除最旧的历史记录\n  Future<void> deleteOldest();\n}\n\n/// 搜索历史数据访问实现类\n///\n/// 基于Hive实现的搜索历史数据访问层\nclass SearchHistoryRepository implements ISearchHistoryRepository {\n  final _searchHistoryBox = GStorage.searchHistory;\n\n  @override\n  List<SearchHistory> getAllHistories() {\n    try {\n      final histories = _searchHistoryBox.values.toList().cast<SearchHistory>();\n      histories.sort((a, b) => b.timestamp - a.timestamp);\n      return histories;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: get all search histories failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return [];\n    }\n  }\n\n  @override\n  Future<bool> saveHistory(String keyword) async {\n    try {\n      final timestamp = DateTime.now().millisecondsSinceEpoch;\n      final history = SearchHistory(keyword, timestamp);\n      await _searchHistoryBox.put(timestamp.toString(), history);\n      return true;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: save search history failed. keyword=$keyword',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> deleteHistory(SearchHistory history) async {\n    try {\n      await _searchHistoryBox.delete(history.key);\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: delete search history failed. key=${history.key}',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  Future<void> clearAllHistories() async {\n    try {\n      await _searchHistoryBox.clear();\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: clear all search histories failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  Future<void> deleteDuplicates(String keyword) async {\n    try {\n      final histories = getAllHistories();\n      final duplicates = histories.where((h) => h.keyword == keyword);\n      for (var history in duplicates) {\n        await deleteHistory(history);\n      }\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: delete duplicate search histories failed. keyword=$keyword',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n\n  @override\n  bool isHistoryFull(int maxCount) {\n    try {\n      return _searchHistoryBox.length >= maxCount;\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: check if search history is full failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n      return false;\n    }\n  }\n\n  @override\n  Future<void> deleteOldest() async {\n    try {\n      final histories = getAllHistories();\n      if (histories.isNotEmpty) {\n        await deleteHistory(histories.last);\n      }\n    } catch (e, stackTrace) {\n      KazumiLogger().e(\n        'GStorage: delete oldest search history failed',\n        error: e,\n        stackTrace: stackTrace,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/request/api.dart",
    "content": "class Api {\n  /// 当前版本\n  static const String version = '2.0.5';\n  /// 规则API级别\n  static const int apiLevel = 6;\n  /// 项目主页\n  static const String projectUrl = \"https://kazumi.app/\";\n  /// Github 项目主页\n  static const String sourceUrl = \"https://github.com/Predidit/Kazumi\";\n  /// 图标作者\n  static const String iconUrl = \"https://www.pixiv.net/users/66219277\";\n  /// 规则仓库\n  static const String pluginShop = 'https://raw.githubusercontent.com/Predidit/KazumiRules/main/';\n  /// 在线升级\n  static const String latestApp =\n      'https://api.github.com/repos/Predidit/Kazumi/releases/latest'; \n  /// Github镜像\n  static const String gitMirror = 'https://ghfast.top/';\n  /// 弹弹官网\n  static const String dandanIndex = 'https://www.dandanplay.com/';\n  /// Bangumi 官网\n  static const String bangumiIndex = 'https://bangumi.tv/';\n\n  /// bangumi API Domain\n  static const String bangumiAPIDomain = 'https://api.bgm.tv';\n  /// 番剧信息\n  static const String bangumiInfoByID = '/v0/subjects/{0}';\n  /// 条目搜索\n  static const String bangumiRankSearch = '/v0/search/subjects?limit={0}&offset={1}';\n  /// 从条目ID获取角色信息\n  static const String bangumiCharacterByID = '/v0/subjects/{0}/characters';\n  /// 从条目ID获取剧集ID\n  static const String bangumiEpisodeByID = '/v0/episodes';\n\n  /// Bangumi Next API Domain\n  static const String bangumiAPINextDomain = 'https://next.bgm.tv';\n  /// 每日放送\n  static const String bangumiCalendar = '/p1/calendar';\n  /// 番剧趋势\n  static const String bangumiTrendsNext = '/p1/trending/subjects';\n  /// 番剧信息\n  static const String bangumiInfoByIDNext = '/p1/subjects/{0}';\n  /// 番剧评论\n  static const String bangumiCommentsByIDNext = '/p1/subjects/{0}/comments?limit={1}&offset={2}';\n  /// 番剧剧集评论\n  static const String bangumiEpisodeCommentsByIDNext = '/p1/episodes/{0}/comments';\n  /// 番剧角色信息\n  static const String bangumiCharacterInfoByCharacterIDNext = '/p1/characters/{0}';\n  /// 番剧角色评论\n  static const String bangumiCharacterCommentsByIDNext = '/p1/characters/{0}/comments';\n  /// 番剧工作人员信息\n  static const String bangumiStaffByIDNext = '/p1/subjects/{0}/staffs/persons';\n\n  /// DanDanPlay API Domain\n  static const String dandanAPIDomain = 'https://api.dandanplay.net';\n  /// 获取弹幕\n  static const String dandanAPIComment = \"/api/v2/comment/\";\n  /// 检索弹弹番剧元数据\n  static const String dandanAPISearch = \"/api/v2/search/anime\";\n  /// 获取弹弹番剧元数据\n  static const String dandanAPIInfo = \"/api/v2/bangumi/\";\n  /// 获取弹弹番剧元数据（通过BGM番剧ID）\n  static const String dandanAPIInfoByBgmBangumiId = \"/api/v2/bangumi/bgmtv/{0}\";\n\n  static String formatUrl(String url, List<dynamic> params) {\n    for (int i = 0; i < params.length; i++) {\n      url = url.replaceAll('{$i}', params[i].toString());\n    }\n    return url;\n  }\n}"
  },
  {
    "path": "lib/request/bangumi.dart",
    "content": "import 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/request/request.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/comments/comment_response.dart';\nimport 'package:kazumi/modules/characters/characters_response.dart';\nimport 'package:kazumi/modules/bangumi/episode_item.dart';\nimport 'package:kazumi/modules/character/character_full_item.dart';\nimport 'package:kazumi/modules/staff/staff_response.dart';\n\nclass BangumiHTTP {\n  // why the api havn't been replaced by getCalendarBySearch?\n  // Because getCalendarBySearch is not stable, it will miss some bangumi items.\n  static Future<List<List<BangumiItem>>> getCalendar() async {\n    List<List<BangumiItem>> bangumiCalendar = [];\n    try {\n      var res = await Request().get(\n        Api.bangumiAPINextDomain + Api.bangumiCalendar,\n      );\n      final jsonData = res.data;\n      for (int i = 1; i <= 7; i++) {\n        List<BangumiItem> bangumiList = [];\n        final jsonList = jsonData['$i'];\n        for (dynamic jsonItem in jsonList) {\n          try {\n            BangumiItem bangumiItem = BangumiItem.fromJson(jsonItem['subject']);\n            bangumiList.add(bangumiItem);\n          } catch (_) {}\n        }\n        bangumiCalendar.add(bangumiList);\n      }\n    } catch (e) {\n      KazumiLogger()\n          .e('Resolve calendar failed', error: e);\n    }\n    return bangumiCalendar;\n  }\n\n  // Get clander by search API, we need a list of strings (the start of the season and the end of the season) eg: [\"2024-07-01\", \"2024-10-01\"]\n  // because the air date is the launch date of the anime, it is usually a few days before the start of the season\n  // So we usually use the start of the season month -1 and the end of the season month -1\n  static Future<List<List<BangumiItem>>> getCalendarBySearch(\n      List<String> dateRange, int limit, int offset) async {\n    List<BangumiItem> bangumiList = [];\n    List<List<BangumiItem>> bangumiCalendar = [];\n    var params = <String, dynamic>{\n      \"keyword\": \"\",\n      \"sort\": \"rank\",\n      \"filter\": {\n        \"type\": [2],\n        \"tag\": [\"日本\"],\n        \"air_date\": [\">=${dateRange[0]}\", \"<${dateRange[1]}\"],\n        \"rank\": [\">0\", \"<=99999\"],\n        \"nsfw\": true\n      }\n    };\n    try {\n      final url = Api.formatUrl(\n          Api.bangumiAPIDomain + Api.bangumiRankSearch, [limit, offset]);\n      final res = await Request().post(\n        url,\n        data: params,\n      );\n      final jsonData = res.data;\n      final jsonList = jsonData['data'];\n      for (dynamic jsonItem in jsonList) {\n        if (jsonItem is Map<String, dynamic>) {\n          bangumiList.add(BangumiItem.fromJson(jsonItem));\n        }\n      }\n    } catch (e) {\n      KazumiLogger()\n          .e('Resolve bangumi list failed', error: e);\n    }\n    try {\n      for (int weekday = 1; weekday <= 7; weekday++) {\n        List<BangumiItem> bangumiDayList = [];\n        for (BangumiItem bangumiItem in bangumiList) {\n          if (bangumiItem.airWeekday == weekday) {\n            bangumiDayList.add(bangumiItem);\n          }\n        }\n        bangumiCalendar.add(bangumiDayList);\n      }\n    } catch (e) {\n      KazumiLogger().e('Network: fetch bangumi item to calendar failed', error: e);\n    }\n    return bangumiCalendar;\n  }\n\n  static Future<List<BangumiItem>> getBangumiList(\n      {int rank = 2, String tag = ''}) async {\n    List<BangumiItem> bangumiList = [];\n    late Map<String, dynamic> params;\n    if (tag == '') {\n      params = <String, dynamic>{\n        'keyword': '',\n        'sort': 'rank',\n        \"filter\": {\n          \"type\": [2],\n          \"tag\": [\"日本\"],\n          \"rank\": [\">$rank\", \"<=1050\"],\n          \"nsfw\": false\n        },\n      };\n    } else {\n      params = <String, dynamic>{\n        'keyword': '',\n        'sort': 'rank',\n        \"filter\": {\n          \"type\": [2],\n          \"tag\": [tag],\n          \"rank\": [\">$rank\", \"<=99999\"],\n          \"nsfw\": false\n        },\n      };\n    }\n    try {\n      final res = await Request().post(\n        Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiRankSearch, [100, 0]),\n        data: params,\n      );\n      final jsonData = res.data;\n      final jsonList = jsonData['data'];\n      for (dynamic jsonItem in jsonList) {\n        if (jsonItem is Map<String, dynamic>) {\n          bangumiList.add(BangumiItem.fromJson(jsonItem));\n        }\n      }\n    } catch (e) {\n      KazumiLogger()\n          .e('Network: resolve bangumi list failed', error: e);\n    }\n    return bangumiList;\n  }\n\n  static Future<List<BangumiItem>> getBangumiTrendsList(\n      {int type = 2, int limit = 24, int offset = 0}) async {\n    List<BangumiItem> bangumiList = [];\n    var params = <String, dynamic>{\n      'type': type,\n      'limit': limit,\n      'offset': offset,\n    };\n    try {\n      final res = await Request().get(\n        Api.bangumiAPINextDomain + Api.bangumiTrendsNext,\n        data: params,\n      );\n      final jsonData = res.data;\n      final jsonList = jsonData['data'];\n      for (dynamic jsonItem in jsonList) {\n        if (jsonItem is Map<String, dynamic>) {\n          bangumiList.add(BangumiItem.fromJson(jsonItem['subject']));\n        }\n      }\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi trends list failed', error: e);\n    }\n    return bangumiList;\n  }\n\n  static Future<List<BangumiItem>> bangumiSearch(String keyword,\n      {List<String> tags = const [],\n      int offset = 0,\n      String sort = 'heat'}) async {\n    List<BangumiItem> bangumiList = [];\n\n    var params = <String, dynamic>{\n      'keyword': keyword,\n      'sort': sort,\n      \"filter\": {\n        \"type\": [2],\n        \"tag\": tags,\n        \"rank\": (sort == 'rank') ? [\">0\", \"<=99999\"] : [\">=0\", \"<=99999\"],\n        \"nsfw\": false\n      },\n    };\n\n    try {\n      final res = await Request().post(\n        Api.formatUrl(\n            Api.bangumiAPIDomain + Api.bangumiRankSearch, [20, offset]),\n        data: params,\n      );\n      final jsonData = res.data;\n      final jsonList = jsonData['data'];\n      for (dynamic jsonItem in jsonList) {\n        if (jsonItem is Map<String, dynamic>) {\n          try {\n            BangumiItem bangumiItem = BangumiItem.fromJson(jsonItem);\n            if (bangumiItem.nameCn != '') {\n              bangumiList.add(bangumiItem);\n            }\n          } catch (e) {\n            KazumiLogger().e('Network: resolve search results failed', error: e);\n          }\n        }\n      }\n    } catch (e) {\n      KazumiLogger().e('Network: unknown search problem', error: e);\n    }\n    return bangumiList;\n  }\n\n  static Future<BangumiItem?> getBangumiInfoByID(int id) async {\n    try {\n      final res = await Request().get(\n        Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiInfoByID, [id]),\n      );\n      return BangumiItem.fromJson(res.data);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi item failed', error: e);\n      return null;\n    }\n  }\n\n  static Future<EpisodeInfo> getBangumiEpisodeByID(int id, int episode) async {\n    EpisodeInfo episodeInfo = EpisodeInfo.fromTemplate();\n    var params = <String, dynamic>{\n      'subject_id': id,\n      'offset': episode - 1,\n      'limit': 1\n    };\n    try {\n      final res = await Request().get(\n        Api.bangumiAPIDomain + Api.bangumiEpisodeByID,\n        data: params,\n      );\n      final jsonData = res.data['data'][0];\n      episodeInfo = EpisodeInfo.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi episode failed', error: e);\n    }\n    return episodeInfo;\n  }\n\n  static Future<CommentResponse> getBangumiCommentsByID(int id,\n      {int offset = 0}) async {\n    CommentResponse commentResponse = CommentResponse.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(Api.bangumiAPINextDomain + Api.bangumiCommentsByIDNext,\n            [id, 20, offset]),\n      );\n      final jsonData = res.data;\n      commentResponse = CommentResponse.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi comments failed', error: e);\n    }\n    return commentResponse;\n  }\n\n  static Future<EpisodeCommentResponse> getBangumiCommentsByEpisodeID(\n      int id) async {\n    EpisodeCommentResponse commentResponse =\n        EpisodeCommentResponse.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(\n            Api.bangumiAPINextDomain + Api.bangumiEpisodeCommentsByIDNext,\n            [id]),\n      );\n      final jsonData = res.data;\n      commentResponse = EpisodeCommentResponse.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi episode comments failed', error: e);\n    }\n    return commentResponse;\n  }\n\n  static Future<CharacterCommentResponse> getCharacterCommentsByCharacterID(\n      int id) async {\n    CharacterCommentResponse commentResponse =\n        CharacterCommentResponse.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(\n            Api.bangumiAPINextDomain + Api.bangumiCharacterCommentsByIDNext,\n            [id]),\n      );\n      final jsonData = res.data;\n      commentResponse = CharacterCommentResponse.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi character comments failed', error: e);\n    }\n    return commentResponse;\n  }\n\n  static Future<StaffResponse> getBangumiStaffByID(int id) async {\n    StaffResponse staffResponse = StaffResponse.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(\n            Api.bangumiAPINextDomain + Api.bangumiStaffByIDNext, [id]),\n      );\n      final jsonData = res.data;\n      staffResponse = StaffResponse.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi staff failed', error: e);\n    }\n    return staffResponse;\n  }\n\n  static Future<CharactersResponse> getCharatersByBangumiID(int id) async {\n    CharactersResponse charactersResponse = CharactersResponse.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiCharacterByID, [id]),\n      );\n      final jsonData = res.data;\n      charactersResponse = CharactersResponse.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve bangumi characters failed', error: e);\n    }\n    return charactersResponse;\n  }\n\n  static Future<CharacterFullItem> getCharacterByCharacterID(int id) async {\n    CharacterFullItem characterFullItem = CharacterFullItem.fromTemplate();\n    try {\n      final res = await Request().get(\n        Api.formatUrl(\n            Api.bangumiAPINextDomain +\n                Api.bangumiCharacterInfoByCharacterIDNext,\n            [id]),\n      );\n      final jsonData = res.data;\n      characterFullItem = CharacterFullItem.fromJson(jsonData);\n    } catch (e) {\n      KazumiLogger().e('Network: resolve character info failed', error: e);\n    }\n    return characterFullItem;\n  }\n}\n"
  },
  {
    "path": "lib/request/damaku.dart",
    "content": "import 'package:kazumi/request/request.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_module.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_search_response.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_episode_response.dart';\nimport 'package:kazumi/utils/string_match.dart';\n\nclass DanmakuRequest {\n  // 从BgmBangumiID获取DanDanBangumiID\n  static Future<int> getDanDanBangumiIDByBgmBangumiID(int bgmBangumiID) async {\n    var path = Api.formatUrl(Api.dandanAPIInfoByBgmBangumiId, [bgmBangumiID]);\n    var endPoint = Api.dandanAPIDomain + path;\n    final res = await Request().get(endPoint,\n        extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'});\n    Map<String, dynamic> jsonData = res.data;\n    DanmakuEpisodeResponse danmakuEpisodeResponse =\n        DanmakuEpisodeResponse.fromJson(jsonData);\n    return danmakuEpisodeResponse.bangumiId;\n  }\n\n  // 从标题获取DanDanBangumiID\n  static Future<int> getBangumiIDByTitle(String title) async {\n    DanmakuSearchResponse danmakuSearchResponse =\n        await getDanmakuSearchResponse(title);\n\n    int bestAnimeId = 0;\n    double maxSimilarity = 0;\n\n    for (var anime in danmakuSearchResponse.animes) {\n      int animeId = anime.animeId;\n      if (animeId >= 100000 || animeId < 2) {\n        continue;\n      }\n\n      String animeTitle = anime.animeTitle;\n      double similarity = calculateSimilarity(animeTitle, title);\n      if (similarity == 1) {\n        KazumiLogger().i('Danmaku: total match $title');\n        return animeId;\n      }\n\n      if (similarity > maxSimilarity) {\n        maxSimilarity = similarity;\n        bestAnimeId = animeId;\n        KazumiLogger().i('Danmaku: match anime danmaku $title --- $animeTitle similarity: $similarity');\n      }\n    }\n\n    return bestAnimeId;\n  }\n\n  // 从BangumiID获取分集ID\n  static Future<DanmakuEpisodeResponse> getDanmakuEpisodesByBangumiID(\n      int bangumiID) async {\n    var path = Api.formatUrl(Api.dandanAPIInfoByBgmBangumiId, [bangumiID]);\n    var endPoint = Api.dandanAPIDomain + path;\n    final res = await Request().get(endPoint,\n        extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'});\n    Map<String, dynamic> jsonData = res.data;\n    DanmakuEpisodeResponse danmakuEpisodeResponse =\n        DanmakuEpisodeResponse.fromJson(jsonData);\n    return danmakuEpisodeResponse;\n  }\n\n  // 从DanDanBangumiID获取分集ID\n  static Future<DanmakuEpisodeResponse> getDanDanEpisodesByDanDanBangumiID(\n      int bangumiID) async {\n    var path = Api.dandanAPIInfo + bangumiID.toString();\n    var endPoint = Api.dandanAPIDomain + path;\n    final res = await Request().get(endPoint,\n        extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'});\n    Map<String, dynamic> jsonData = res.data;\n    DanmakuEpisodeResponse danmakuEpisodeResponse =\n        DanmakuEpisodeResponse.fromJson(jsonData);\n    return danmakuEpisodeResponse;\n  }\n\n  // 从标题检索DanDan番剧数据库\n  static Future<DanmakuSearchResponse> getDanmakuSearchResponse(\n      String title) async {\n    var path = Api.dandanAPISearch;\n    var endPoint = Api.dandanAPIDomain + path;\n    Map<String, String> keywordMap = {\n      'keyword': title,\n    };\n\n    final res = await Request().get(endPoint,\n        data: keywordMap,\n        extra: {'customError': '弹幕检索错误: 获取弹幕番剧ID失败'});\n    Map<String, dynamic> jsonData = res.data;\n    DanmakuSearchResponse danmakuSearchResponse =\n        DanmakuSearchResponse.fromJson(jsonData);\n    return danmakuSearchResponse;\n  }\n\n  static Future<List<Danmaku>> getDanDanmaku(int bangumiID, int episode) async {\n    List<Danmaku> danmakus = [];\n    if (bangumiID == 0) {\n      return danmakus;\n    }\n    // 这里猜测了弹弹Play的分集命名规则，例如上面的番剧ID为1758，第一集弹幕库ID大概率为17580001，但是此命名规则并没有体现在官方API文档里，保险的做法是请求 Api.dandanInfo\n    var path = Api.dandanAPIComment +\n        bangumiID.toString() +\n        episode.toString().padLeft(4, '0');\n    var endPoint = Api.dandanAPIDomain + path;\n    Map<String, String> withRelated = {\n      'withRelated': 'true',\n    };\n    KazumiLogger().i(\"Danmaku: final request URL $endPoint\");\n    final res = await Request().get(endPoint,\n        data: withRelated,\n        extra: {'customError': '弹幕检索错误: 获取弹幕失败'});\n\n    Map<String, dynamic> jsonData = res.data;\n    List<dynamic> comments = jsonData['comments'];\n\n    for (var comment in comments) {\n      Danmaku danmaku = Danmaku.fromJson(comment);\n      danmakus.add(danmaku);\n    }\n    return danmakus;\n  }\n\n  static Future<List<Danmaku>> getDanDanmakuByEpisodeID(int episodeID) async {\n    var path = Api.dandanAPIComment + episodeID.toString();\n    var endPoint = Api.dandanAPIDomain + path;\n    List<Danmaku> danmakus = [];\n    Map<String, String> withRelated = {\n      'withRelated': 'true',\n    };\n    final res = await Request().get(endPoint,\n        data: withRelated,\n        extra: {'customError': '弹幕检索错误: 获取弹幕失败'});\n    Map<String, dynamic> jsonData = res.data;\n    List<dynamic> comments = jsonData['comments'];\n\n    for (var comment in comments) {\n      Danmaku danmaku = Danmaku.fromJson(comment);\n      danmakus.add(danmaku);\n    }\n    return danmakus;\n  }\n}\n"
  },
  {
    "path": "lib/request/interceptor.dart",
    "content": "import 'package:dio/dio.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/mortis.dart';\nimport 'package:kazumi/utils/constants.dart';\n\nclass ApiInterceptor extends Interceptor {\n  static Box setting = GStorage.setting;\n  @override\n  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\n    // Github mirror\n    if (options.path.contains('github')) {\n      bool enableGitProxy =\n          setting.get(SettingBoxKey.enableGitProxy, defaultValue: false);\n      if (enableGitProxy) {\n        options.path = Api.gitMirror + options.path;\n      }\n    }\n    if (options.path.contains(Api.dandanAPIDomain)) {\n      var timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;\n      options.headers = {\n        'user-agent': Utils.getRandomUA(),\n        'referer': '',\n        'X-Auth': 1,\n        'X-AppId': mortis['id'],\n        'X-Timestamp': timestamp,\n        'X-Signature': Utils.generateDandanSignature(\n            Uri.parse(options.path).path, timestamp),\n      };\n    }\n    if (options.path.contains(Api.bangumiAPIDomain) ||\n        options.path.contains(Api.bangumiAPINextDomain)) {\n      options.headers = bangumiHTTPHeader;\n    }\n    handler.next(options);\n  }\n\n  @override\n  void onResponse(Response response, ResponseInterceptorHandler handler) {\n    handler.next(response);\n  }\n\n  @override\n  void onError(DioException err, ErrorInterceptorHandler handler) async {\n    String url = err.requestOptions.uri.toString();\n    if (!url.contains('heartBeat') &&\n        err.requestOptions.extra['customError'] != '') {\n      if (err.requestOptions.extra['customError'] == null) {\n        KazumiDialog.showToast(\n          message: await dioError(err),\n        );\n      } else {\n        KazumiDialog.showToast(\n          message: err.requestOptions.extra['customError'],\n        );\n      }\n    }\n    super.onError(err, handler);\n  }\n\n  static Future<String> dioError(DioException error) async {\n    bool proxyEnable =\n        await setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (proxyEnable) {\n      return '代理连接异常，请检查代理设置';\n    }\n    switch (error.type) {\n      case DioExceptionType.badCertificate:\n        return '证书有误！';\n      case DioExceptionType.badResponse:\n        return '服务器异常，请稍后重试！';\n      case DioExceptionType.cancel:\n        return '请求已被取消，请重新请求';\n      case DioExceptionType.connectionError:\n        return '连接错误，请检查网络设置';\n      case DioExceptionType.connectionTimeout:\n        return '网络连接超时，请检查网络设置';\n      case DioExceptionType.receiveTimeout:\n        return '响应超时，请稍后重试！';\n      case DioExceptionType.sendTimeout:\n        return '发送请求超时，请检查网络设置';\n      case DioExceptionType.unknown:\n        final String res = await checkConnect();\n        return '$res 网络异常';\n    }\n  }\n\n  static Future<String> checkConnect() async {\n    final connectivityResult = await Connectivity().checkConnectivity();\n    if (connectivityResult.contains(ConnectivityResult.mobile)) {\n      return '正在使用移动流量';\n    }\n    if (connectivityResult.contains(ConnectivityResult.wifi)) {\n      return '正在使用wifi';\n    }\n    if (connectivityResult.contains(ConnectivityResult.ethernet)) {\n      return '正在使用局域网';\n    }\n    if (connectivityResult.contains(ConnectivityResult.vpn)) {\n      return '正在使用代理网络';\n    }\n    if (connectivityResult.contains(ConnectivityResult.other)) {\n      return '正在使用其他网络';\n    }\n    if (connectivityResult.contains(ConnectivityResult.none)) {\n      return '未连接到任何网络';\n    }\n    return '';\n  }\n}\n"
  },
  {
    "path": "lib/request/plugin.dart",
    "content": "import 'dart:convert';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/request/request.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:kazumi/modules/plugin/plugin_http_module.dart';\n\nclass PluginHTTP {\n  static Future<List<PluginHTTPItem>> getPluginList() async {\n    List<PluginHTTPItem> pluginHTTPItemList = [];\n    try {\n      var res = await Request().get('${Api.pluginShop}index.json');\n      final jsonData = json.decode(res.data);\n      for (dynamic pluginJsonItem in jsonData) {\n        try {\n          PluginHTTPItem pluginHTTPItem = PluginHTTPItem.fromJson(pluginJsonItem);\n          pluginHTTPItemList.add(pluginHTTPItem);\n        } catch (_) {}\n      }\n    } catch (e) {\n      KazumiLogger().e('Plugin: getPluginList error: ${e.toString()}');\n    }\n    return pluginHTTPItemList;\n  }\n\n  static Future<Plugin?> getPlugin(String name) async {\n    Plugin? plugin;\n    try {\n      var res = await Request().get('${Api.pluginShop}$name.json');\n      final jsonData = json.decode(res.data);\n      plugin = Plugin.fromJson(jsonData);\n    } catch(e) {\n      KazumiLogger().e('Plugin: getPlugin error: ${e.toString()}');\n    }\n    return plugin;\n  }\n}"
  },
  {
    "path": "lib/request/query_manager.dart",
    "content": "import 'dart:async';\nimport 'package:kazumi/modules/search/plugin_search_module.dart';\nimport 'package:kazumi/plugins/plugins.dart';\nimport 'package:flutter_modular/flutter_modular.dart';\nimport 'package:kazumi/pages/info/info_controller.dart';\nimport 'package:kazumi/plugins/plugins_controller.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass QueryManager {\n  QueryManager({\n    required this.infoController,\n  });\n\n  final InfoController infoController;\n  final PluginsController pluginsController = Modular.get<PluginsController>();\n  StreamController? _controller;\n  bool _isCancelled = false;\n\n  Future<void> querySource(String keyword, String pluginName) async {\n    for (PluginSearchResponse pluginSearchResponse\n        in infoController.pluginSearchResponseList) {\n      if (pluginSearchResponse.pluginName == pluginName) {\n        infoController.pluginSearchResponseList.remove(pluginSearchResponse);\n        break;\n      }\n    }\n    if (infoController.pluginSearchStatus.containsKey(pluginName)) {\n      infoController.pluginSearchStatus[pluginName] = 'pending';\n    }\n    for (Plugin plugin in pluginsController.pluginList) {\n      if (plugin.name == pluginName) {\n        plugin.queryBangumi(keyword, shouldRethrow: true).then((result) {\n          if (_isCancelled) {\n            return;\n          }\n\n          infoController.pluginSearchStatus[plugin.name] = 'success';\n          if (result.data.isNotEmpty) {\n            pluginsController.validityTracker.markSearchValid(plugin.name);\n          }\n          infoController.pluginSearchResponseList.add(result);\n        }).catchError((error) {\n          if (_isCancelled) {\n            return;\n          }\n\n          if (error is CaptchaRequiredException) {\n            KazumiLogger().w('QueryManager: captcha required for ${error.pluginName}');\n            infoController.pluginSearchStatus[error.pluginName] = 'captcha';\n          } else if (error is NoResultException) {\n            KazumiLogger().i('QueryManager: no results for ${error.pluginName}');\n            infoController.pluginSearchStatus[error.pluginName] = 'noResult';\n          } else {\n            final name = error is SearchErrorException ? error.pluginName : plugin.name;\n            KazumiLogger().w('QueryManager: search error for $name');\n            infoController.pluginSearchStatus[name] = 'error';\n          }\n        });\n      }\n    }\n  }\n\n  Future<void> queryAllSource(String keyword) async {\n    _controller = StreamController();\n    infoController.pluginSearchResponseList.clear();\n\n    for (Plugin plugin in pluginsController.pluginList) {\n      infoController.pluginSearchStatus[plugin.name] = 'pending';\n    }\n\n    for (Plugin plugin in pluginsController.pluginList) {\n      if (_isCancelled) return;\n\n      plugin.queryBangumi(keyword, shouldRethrow: true).then((result) {\n        if (_isCancelled) {\n          return;\n        }\n\n        infoController.pluginSearchStatus[plugin.name] = 'success';\n        if (result.data.isNotEmpty) {\n          pluginsController.validityTracker.markSearchValid(plugin.name);\n        }\n        _controller?.add(result);\n      }).catchError((error) {\n        if (_isCancelled) {\n          return;\n        }\n\n        if (error is CaptchaRequiredException) {\n          KazumiLogger().w('QueryManager: captcha required for ${error.pluginName}');\n          infoController.pluginSearchStatus[error.pluginName] = 'captcha';\n        } else if (error is NoResultException) {\n          KazumiLogger().i('QueryManager: no results for ${error.pluginName}');\n          infoController.pluginSearchStatus[error.pluginName] = 'noResult';\n        } else {\n          final name = error is SearchErrorException ? error.pluginName : plugin.name;\n          KazumiLogger().w('QueryManager: search error for $name');\n          infoController.pluginSearchStatus[name] = 'error';\n        }\n      });\n    }\n\n    await for (var result in _controller!.stream) {\n      if (_isCancelled) break;\n\n      infoController.pluginSearchResponseList.add(result);\n    }\n  }\n\n  void cancel() {\n    _isCancelled = true;\n    if (_controller != null && !_controller!.isClosed) {\n      _controller!.close();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/request/request.dart",
    "content": "import 'dart:io';\n\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:kazumi/request/interceptor.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:hive_ce/hive.dart';\n\nclass Request {\n  static final Request _instance = Request._internal();\n  static late final Dio dio;\n  static Box setting = GStorage.setting;\n  factory Request() => _instance;\n\n  // 初始化 （一般只在应用启动时调用）\n  static Future<void> setCookie() async {\n    setOptionsHeaders();\n    // 初始化时检查并设置代理\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (proxyEnable) {\n      setProxy();\n    }\n  }\n\n  // 设置请求头\n  static void setOptionsHeaders() {\n    dio.options.headers['referer'] = '';\n    dio.options.headers['user-agent'] = Utils.getRandomUA();\n  }\n\n  // 设置代理（仅支持 HTTP 代理）\n  static void setProxy() {\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) {\n      disableProxy();\n      return;\n    }\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n\n    final parsed = ProxyUtils.parseProxyUrl(proxyUrl);\n    if (parsed == null) {\n      KazumiLogger().w('Proxy: 代理地址格式错误或为空');\n      return;\n    }\n\n    final (proxyHost, proxyPort) = parsed;\n\n    dio.httpClientAdapter = IOHttpClientAdapter(\n      createHttpClient: () {\n        final HttpClient client = HttpClient();\n        client.findProxy = (Uri uri) {\n          return 'PROXY $proxyHost:$proxyPort';\n        };\n        // 忽略证书验证\n        client.badCertificateCallback =\n            (X509Certificate cert, String host, int port) => true;\n        return client;\n      },\n    );\n    KazumiLogger().i('Proxy: HTTP 代理设置成功 $proxyHost:$proxyPort');\n  }\n\n  // 禁用代理\n  static void disableProxy() {\n    dio.httpClientAdapter = IOHttpClientAdapter(\n        createHttpClient: () {\n          final HttpClient client = HttpClient();\n          return client;\n        },\n      );\n    KazumiLogger().i('Proxy: 代理已禁用');\n  }\n\n  Request._internal() {\n    //BaseOptions、Options、RequestOptions 都可以配置参数，优先级别依次递增，且可以根据优先级别覆盖参数\n    BaseOptions options = BaseOptions(\n      //请求基地址,可以包含子路径\n      baseUrl: '',\n      //连接服务器超时时间，单位是毫秒.\n      connectTimeout: const Duration(milliseconds: 12000),\n      //响应流上前后两次接受到数据的间隔，单位为毫秒。\n      receiveTimeout: const Duration(milliseconds: 12000),\n      //Http请求头.\n      headers: {},\n    );\n\n    // enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy,\n    //     defaultValue: false) as bool;\n\n    dio = Dio(options);\n    // debugPrint('Dio 初始化完成');\n    \n    // if (enableSystemProxy) {\n    //   setProxy();\n    //   debugPrint('系统代理启用');\n    // }\n\n    // 拦截器\n    dio.interceptors.add(ApiInterceptor());\n\n    // 日志拦截器 输出请求、响应内容\n    dio.interceptors.add(LogInterceptor(\n      request: false,\n      requestHeader: false,\n      responseHeader: false,\n    ));\n\n    dio.transformer = BackgroundTransformer();\n    dio.options.validateStatus = (int? status) {\n      return status! >= 200 && status < 300;\n    };\n  }\n\n  Future<Response> get(url, {data, options, cancelToken, extra, bool shouldRethrow = false}) async {\n    Response response;\n    ResponseType resType = ResponseType.json;\n    options ??= Options();\n    if (extra != null) {\n      resType = extra!['resType'] ?? ResponseType.json;\n      if (extra['ua'] != null) {\n        options.headers = {'user-agent': headerUa(type: extra['ua'])};\n      }\n      if (extra['customError'] != null) {\n        options.extra = {'customError': extra['customError']};\n      }\n    }\n    options.responseType = resType;\n    try {\n      response = await dio.get(\n        url,\n        queryParameters: data,\n        options: options,\n        cancelToken: cancelToken,\n      );\n      return response;\n    } on DioException catch (e) {\n      if (shouldRethrow) {\n        rethrow;\n      }\n      Response errResponse = Response(\n        data: {\n          'message': await ApiInterceptor.dioError(e)\n        }, // 将自定义 Map 数据赋值给 Response 的 data 属性\n        statusCode: 200,\n        requestOptions: RequestOptions(),\n      );\n      return errResponse;\n    }\n  }\n\n  Future<Response> post(url, {data, queryParameters, options, cancelToken, extra, bool shouldRethrow = false}) async {\n    // print('post-data: $data');\n    Response response;\n    try {\n      response = await dio.post(\n        url,\n        data: data,\n        queryParameters: queryParameters,\n        options: options,\n        cancelToken: cancelToken,\n      );\n      // print('post success: ${response.data}');\n      return response;\n    } on DioException catch (e) {\n      if (shouldRethrow) {\n        rethrow;\n      }\n      Response errResponse = Response(\n        data: {\n          'message': await ApiInterceptor.dioError(e)\n        }, // 将自定义 Map 数据赋值给 Response 的 data 属性\n        statusCode: 200,\n        requestOptions: RequestOptions(),\n      );\n      return errResponse;\n    }\n  }\n\n  String headerUa({type = 'mob'}) {\n    return Utils.getRandomUA();\n  }\n}"
  },
  {
    "path": "lib/shaders/shaders_controller.dart",
    "content": "import 'dart:io';\nimport 'package:mobx/mobx.dart';\nimport 'package:flutter/services.dart' show rootBundle, AssetManifest;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:path/path.dart' as path;\nimport 'package:kazumi/utils/logger.dart';\n\npart 'shaders_controller.g.dart';\n\nclass ShadersController = _ShadersController with _$ShadersController;\n\nabstract class _ShadersController with Store {\n  late Directory shadersDirectory;\n\n  Future<void> copyShadersToExternalDirectory() async {\n    final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n    final assets = assetManifest.listAssets();\n    final directory = await getApplicationSupportDirectory();\n    shadersDirectory = Directory(path.join(directory.path, 'anime_shaders'));\n\n    if (!await shadersDirectory.exists()) {\n      await shadersDirectory.create(recursive: true);\n      KazumiLogger()\n          .i('ShaderManager: Create GLSL Shader: ${shadersDirectory.path}');\n    }\n\n    final shaderFiles = assets.where((String asset) =>\n        asset.startsWith('assets/shaders/') && asset.endsWith('.glsl'));\n\n    int copiedFilesCount = 0;\n\n    for (var filePath in shaderFiles) {\n      final fileName = filePath.split('/').last;\n      final targetFile = File(path.join(shadersDirectory.path, fileName));\n      if (await targetFile.exists()) {\n        KazumiLogger()\n            .i('ShaderManager: GLSL Shader exists, skip: ${targetFile.path}');\n        continue;\n      }\n\n      try {\n        final data = await rootBundle.load(filePath);\n        final List<int> bytes = data.buffer.asUint8List();\n        await targetFile.writeAsBytes(bytes);\n        copiedFilesCount++;\n        KazumiLogger().i('ShaderManager: Copy: ${targetFile.path}');\n      } catch (e) {\n        KazumiLogger().e('ShaderManager: Copy: ($filePath)', error: e);\n      }\n    }\n\n    KazumiLogger().i(\n        'ShaderManager: $copiedFilesCount GLSL files copied to ${shadersDirectory.path}');\n  }\n}\n"
  },
  {
    "path": "lib/shaders/shaders_controller.g.dart",
    "content": "// GENERATED CODE - DO NOT MODIFY BY HAND\n\npart of 'shaders_controller.dart';\n\n// **************************************************************************\n// StoreGenerator\n// **************************************************************************\n\n// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers\n\nmixin _$ShadersController on _ShadersController, Store {\n  @override\n  String toString() {\n    return '''\n\n    ''';\n  }\n}\n"
  },
  {
    "path": "lib/utils/anime_season.dart",
    "content": "/// This class asks for DateTime to get a string to indicate seasonal anime\nclass AnimeSeason {\n  late DateTime _date;\n  final _seasons = ['冬季', '春季', '夏季', '秋季'];\n\n  AnimeSeason(DateTime date) {\n    _date = date;\n  }\n\n  List<int> _getYearAndSeason(DateTime dt) {\n    int year = dt.year;\n    int month = dt.month;\n\n    int season;\n    if ((month == 1) || (month == 2) || (month == 3)) {\n      season = 0;\n    } else if ((month == 4) || (month == 5) || (month == 6)) {\n      season = 1;\n    } else if ((month == 7) || (month == 8) || (month == 9)) {\n      season = 2;\n    } else {\n      season = 3;\n    }\n\n    return [year, season];\n  }\n\n  // Convert the DateTime to a List containing two strings (the start of the season -1 and the end of the season -1 ) eg: 2024-09-23 -> ['2024-06-01', '2024-09-01']\n  // why -1? because the air date is the launch date of the anime, it is usually a few days before the start of the season\n  List<String> toSeasonStartAndEnd() {\n    var yas = _getYearAndSeason(_date);\n    int year = yas[0];\n    int season = yas[1];\n\n    var end = DateTime(year, (season + 1) * 3, 1);\n\n    int startMonth = season * 3;\n    if (startMonth == 0) {\n      startMonth = 12;\n      year--;\n    }\n\n    var start = DateTime(year, startMonth, 1);\n    return [start.toString(), end.toString()];\n  }\n\n  @override\n  String toString() {\n    var yas = _getYearAndSeason(_date);\n\n    return '${yas[0]}年${_seasons[yas[1]]}新番';\n  }\n}\n"
  },
  {
    "path": "lib/utils/auto_updater.dart",
    "content": "import 'dart:io';\nimport 'package:dio/dio.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:open_filex/open_filex.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\n/// 安装类型枚举\nenum InstallationType {\n  windowsMsix, // Kazumi_windows_1.7.5.msix\n  windowsPortable, // Kazumi_windows_1.7.5.zip\n  linuxDeb, // Kazumi_linux_1.7.5_amd64.deb\n  linuxTar, // Kazumi_linux_1.7.5_amd64.tar.gz\n  macosDmg, // Kazumi_macos_1.7.5.dmg\n  androidApk, // Kazumi_android_1.7.5.apk\n  ios, // iOS App\n  unknown,\n}\n\n/// 更新信息类\nclass UpdateInfo {\n  final String version;\n  final String description;\n  final String downloadUrl;\n  final String releaseNotes;\n  final String publishedAt;\n  final InstallationType? installationType;\n  final List<InstallationType> availableInstallationTypes;\n  final List<dynamic> assets;\n\n  UpdateInfo({\n    required this.version,\n    required this.description,\n    required this.downloadUrl,\n    required this.releaseNotes,\n    required this.publishedAt,\n    this.installationType,\n    this.availableInstallationTypes = const [],\n    this.assets = const [],\n  });\n\n  /// 获取默认的安装类型（第一个可用类型）\n  InstallationType get recommendedInstallationType {\n    if (availableInstallationTypes.isNotEmpty) {\n      return availableInstallationTypes.first;\n    }\n    return installationType ?? InstallationType.unknown;\n  }\n}\n\nclass AutoUpdater {\n  static final AutoUpdater _instance = AutoUpdater._internal();\n\n  factory AutoUpdater() => _instance;\n\n  AutoUpdater._internal();\n\n  final Dio _dio = Dio();\n\n  Box get setting => GStorage.setting;\n\n  /// 检测所有可能的安装类型\n  Future<List<InstallationType>> _detectAvailableInstallationTypes() async {\n    List<InstallationType> availableTypes = [];\n\n    try {\n      if (Platform.isWindows) {\n        // Windows 平台支持 MSIX 和 ZIP 便携版\n        availableTypes.add(InstallationType.windowsMsix);\n        availableTypes.add(InstallationType.windowsPortable);\n      } else if (Platform.isLinux) {\n        // Linux 平台支持 DEB 和 TAR.GZ\n        availableTypes.add(InstallationType.linuxDeb);\n        availableTypes.add(InstallationType.linuxTar);\n      } else if (Platform.isMacOS) {\n        // macOS 平台支持 DMG\n        availableTypes.add(InstallationType.macosDmg);\n      } else if (Platform.isIOS) {\n        // iOS 平台通过 Github\n        availableTypes.add(InstallationType.ios);\n      } else if (Platform.isAndroid) {\n        // Android 平台支持 APK\n        availableTypes.add(InstallationType.androidApk);\n      }\n    } catch (e) {\n      KazumiLogger().w('Update: detect installation types failed', error: e);\n    }\n\n    if (availableTypes.isEmpty) {\n      availableTypes.add(InstallationType.unknown);\n    }\n\n    return availableTypes;\n  }\n\n  /// 检查是否有新版本可用\n  Future<UpdateInfo?> checkForUpdates() async {\n    try {\n      final response = await _dio.get(Api.latestApp);\n      final data = response.data;\n\n      if (data == null || !data.containsKey('tag_name')) {\n        throw Exception('无效的响应数据');\n      }\n\n      final remoteVersion = data['tag_name'] as String;\n      final currentVersion = Api.version;\n\n      if (Utils.needUpdate(currentVersion, remoteVersion)) {\n        final availableTypes = await _detectAvailableInstallationTypes();\n\n        return UpdateInfo(\n          version: remoteVersion,\n          description: data['body'] ?? '发现新版本',\n          downloadUrl: '',\n          // 将在用户选择安装类型后填充\n          releaseNotes: data['html_url'] ?? '',\n          publishedAt: data['published_at'] ?? '',\n          installationType: availableTypes.first,\n          // 保持兼容性\n          availableInstallationTypes: availableTypes,\n          assets: data['assets'] ?? [],\n        );\n      }\n\n      return null;\n    } catch (e) {\n      KazumiLogger().e('Update: check for updates failed', error: e);\n      rethrow;\n    }\n  }\n\n  /// 自动检查更新（仅在启用自动更新时）\n  Future<void> autoCheckForUpdates() async {\n    final autoUpdate =\n        setting.get(SettingBoxKey.autoUpdate, defaultValue: true);\n    if (!autoUpdate) return;\n\n    try {\n      final updateInfo = await checkForUpdates();\n      if (updateInfo != null) {\n        _showUpdateDialog(updateInfo, isAutoCheck: true);\n      }\n    } catch (e) {\n      // 自动检查失败时不显示错误\n      KazumiLogger().w('Update: auto check for updates failed', error: e);\n    }\n  }\n\n  /// 手动检查更新\n  Future<void> manualCheckForUpdates() async {\n    try {\n      final updateInfo = await checkForUpdates();\n      if (updateInfo != null) {\n        _showUpdateDialog(updateInfo, isAutoCheck: false);\n      } else {\n        KazumiDialog.showToast(message: '当前已经是最新版本！');\n      }\n    } catch (e) {\n      KazumiDialog.showToast(message: '检查更新失败');\n    }\n  }\n\n  /// 显示更新对话框\n  void _showUpdateDialog(UpdateInfo updateInfo, {bool isAutoCheck = false}) {\n    KazumiDialog.show(\n      builder: (context) {\n        return AlertDialog(\n          title: Text('发现新版本 ${updateInfo.version}'),\n          content: SingleChildScrollView(\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(updateInfo.description),\n                if (updateInfo.publishedAt.isNotEmpty) ...[\n                  const SizedBox(height: 8),\n                  Text(\n                    '发布时间: ${Utils.formatDate(updateInfo.publishedAt)}',\n                    style: Theme.of(context).textTheme.bodySmall,\n                  ),\n                ],\n                const SizedBox(height: 8),\n                if (!Platform.isLinux && !Platform.isIOS) ...[\n                  Container(\n                    padding: const EdgeInsets.all(8),\n                    decoration: BoxDecoration(\n                      color:\n                          Theme.of(context).colorScheme.surfaceContainerHighest,\n                      borderRadius: BorderRadius.circular(4),\n                    ),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Text(\n                          '选择安装类型:',\n                          style: Theme.of(context).textTheme.labelSmall,\n                        ),\n                        const SizedBox(height: 8),\n                        ...updateInfo.availableInstallationTypes.map((type) {\n                          return Container(\n                            margin: const EdgeInsets.symmetric(vertical: 2),\n                            child: Material(\n                              color: Colors.transparent,\n                              child: InkWell(\n                                borderRadius: BorderRadius.circular(4),\n                                onTap: () {\n                                  KazumiDialog.dismiss();\n                                  _downloadUpdateWithType(updateInfo, type);\n                                },\n                                child: Container(\n                                  padding: const EdgeInsets.symmetric(\n                                      horizontal: 12, vertical: 8),\n                                  decoration: BoxDecoration(\n                                    border: Border.all(\n                                      color: Theme.of(context)\n                                          .colorScheme\n                                          .outline\n                                          .withValues(alpha: 0.3),\n                                    ),\n                                    borderRadius: BorderRadius.circular(4),\n                                  ),\n                                  child: Row(\n                                    children: [\n                                      Icon(\n                                        Icons.download,\n                                        size: 16,\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .primary,\n                                      ),\n                                      const SizedBox(width: 8),\n                                      Expanded(\n                                        child: Text(\n                                          _getInstallationTypeDescription(type),\n                                          style: Theme.of(context)\n                                              .textTheme\n                                              .bodySmall,\n                                        ),\n                                      ),\n                                      Icon(\n                                        Icons.arrow_forward_ios,\n                                        size: 12,\n                                        color: Theme.of(context)\n                                            .colorScheme\n                                            .outline,\n                                      ),\n                                    ],\n                                  ),\n                                ),\n                              ),\n                            ),\n                          );\n                        }),\n                      ],\n                    ),\n                  ),\n                ],\n              ],\n            ),\n          ),\n          actions: [\n            if (isAutoCheck)\n              TextButton(\n                onPressed: () {\n                  setting.put(SettingBoxKey.autoUpdate, false);\n                  KazumiDialog.dismiss();\n                  KazumiDialog.showToast(message: '已关闭自动更新');\n                },\n                child: Text(\n                  '关闭自动更新',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline),\n                ),\n              ),\n            TextButton(\n              onPressed: () => KazumiDialog.dismiss(),\n              child: Text(\n                '稍后提醒',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            if (updateInfo.releaseNotes.isNotEmpty)\n              TextButton(\n                onPressed: () {\n                  launchUrl(Uri.parse(updateInfo.releaseNotes),\n                      mode: LaunchMode.externalApplication);\n                },\n                child: const Text('查看详情'),\n              ),\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n                // 直接使用第一个可用的安装类型\n                if (updateInfo.availableInstallationTypes.isNotEmpty) {\n                  _downloadUpdateWithType(\n                      updateInfo, updateInfo.availableInstallationTypes.first);\n                }\n              },\n              child: const Text('立即更新'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  /// 获取安装类型的描述\n  String _getInstallationTypeDescription(InstallationType type) {\n    switch (type) {\n      case InstallationType.windowsMsix:\n        return 'Windows MSIX 包';\n      case InstallationType.windowsPortable:\n        return 'Windows 便携版 (ZIP)';\n      case InstallationType.linuxDeb:\n        return 'Linux DEB 包';\n      case InstallationType.linuxTar:\n        return 'Linux TAR 包';\n      case InstallationType.macosDmg:\n        return 'macOS DMG 镜像';\n      case InstallationType.androidApk:\n        return 'Android APK';\n      case InstallationType.ios:\n        return 'iOS ipa';\n      case InstallationType.unknown:\n        return '未知安装类型';\n    }\n  }\n\n  /// 根据选择的类型下载更新\n  Future<void> _downloadUpdateWithType(\n      UpdateInfo updateInfo, InstallationType selectedType) async {\n    try {\n      // iOS 和 Linux 直接跳转到 Release 页面\n      if (selectedType == InstallationType.ios ||\n          selectedType == InstallationType.linuxDeb ||\n          selectedType == InstallationType.linuxTar) {\n        String releaseUrl = updateInfo.releaseNotes;\n        if (releaseUrl.isEmpty) {\n          releaseUrl = Api.latestApp;\n        }\n        launchUrl(Uri.parse(releaseUrl), mode: LaunchMode.externalApplication);\n        return;\n      }\n\n      final downloadUrl =\n          await _getDownloadUrlForType(updateInfo.assets, selectedType);\n      if (downloadUrl.isEmpty) {\n        KazumiDialog.showToast(\n            message:\n                '没有找到 ${_getInstallationTypeDescription(selectedType)} 的下载链接');\n        return;\n      }\n\n      // 获取文件的 SHA256 哈希值用于验证\n      final expectedHash =\n          _getFileHashFromAssets(updateInfo.assets, downloadUrl);\n\n      // 创建一个临时的 UpdateInfo 对象用于下载\n      final downloadInfo = UpdateInfo(\n        version: updateInfo.version,\n        description: updateInfo.description,\n        downloadUrl: downloadUrl,\n        releaseNotes: updateInfo.releaseNotes,\n        publishedAt: updateInfo.publishedAt,\n        installationType: selectedType,\n        availableInstallationTypes: [selectedType],\n        assets: updateInfo.assets,\n      );\n\n      _downloadUpdate(downloadInfo, expectedHash);\n    } catch (e) {\n      KazumiDialog.showToast(message: '下载失败: ${e.toString()}');\n      KazumiLogger().e('Update: download update failed', error: e);\n    }\n  }\n\n  /// 下载更新\n  Future<void> _downloadUpdate(\n      UpdateInfo updateInfo, String expectedHash) async {\n    if (updateInfo.downloadUrl.isEmpty) {\n      KazumiDialog.showToast(message: '没有找到合适的下载链接');\n      return;\n    }\n\n    // 显示下载进度对话框\n    KazumiDialog.show(\n      clickMaskDismiss: false,\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('正在下载更新'),\n          content: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              ValueListenableBuilder<double>(\n                valueListenable: _downloadProgress,\n                builder: (context, value, child) {\n                  return Column(\n                    children: [\n                      LinearProgressIndicator(value: value),\n                      const SizedBox(height: 8),\n                      Text('${(value * 100).toStringAsFixed(1)}%'),\n                    ],\n                  );\n                },\n              ),\n            ],\n          ),\n          actions: [\n            TextButton(\n              onPressed: () {\n                _cancelDownload();\n                KazumiDialog.dismiss();\n              },\n              child: const Text('取消'),\n            ),\n          ],\n        );\n      },\n    );\n\n    try {\n      final downloadPath = await _downloadFile(\n          updateInfo.downloadUrl, updateInfo.version, expectedHash);\n\n      // 不自动关闭对话框，而是显示下载完成状态\n      _showDownloadCompleteDialog(downloadPath, updateInfo);\n    } catch (e) {\n      KazumiDialog.dismiss();\n\n      // 显示详细的错误信息\n      String errorMessage = '下载失败';\n      if (e.toString().contains('Permission denied') ||\n          e.toString().contains('Operation not permitted')) {\n        errorMessage = '权限不足，文件已保存到应用临时目录';\n      } else if (e.toString().contains('No space left')) {\n        errorMessage = '磁盘空间不足';\n      } else if (e.toString().contains('Network')) {\n        errorMessage = '网络连接错误';\n      } else if (e.toString().contains('文件完整性验证失败')) {\n        errorMessage = '文件完整性验证失败，可能是网络传输错误';\n      }\n\n      KazumiDialog.show(\n        builder: (context) {\n          return AlertDialog(\n            title: const Text('下载失败'),\n            content: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(errorMessage),\n                const SizedBox(height: 8),\n                Text(\n                  '错误详情: ${e.toString()}',\n                  style: Theme.of(context).textTheme.bodySmall,\n                ),\n              ],\n            ),\n            actions: [\n              TextButton(\n                onPressed: () => KazumiDialog.dismiss(),\n                child: const Text('确定'),\n              ),\n              TextButton(\n                onPressed: () {\n                  KazumiDialog.dismiss();\n                  // 重新尝试下载\n                  _downloadUpdate(updateInfo, expectedHash);\n                },\n                child: const Text('重试'),\n              ),\n            ],\n          );\n        },\n      );\n\n      KazumiLogger().e('Update: download update failed', error: e);\n    }\n  }\n\n  final ValueNotifier<double> _downloadProgress = ValueNotifier(0.0);\n  CancelToken? _cancelToken;\n\n  void _cancelDownload() {\n    _cancelToken?.cancel();\n  }\n\n  /// 显示下载完成对话框\n  void _showDownloadCompleteDialog(String filePath, UpdateInfo updateInfo) {\n    // 替换当前的下载进度对话框内容\n    KazumiDialog.dismiss();\n\n    KazumiDialog.show(\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('下载完成'),\n          content: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Row(\n                children: [\n                  Icon(\n                    Icons.check_circle,\n                    color: Theme.of(context).colorScheme.primary,\n                    size: 20,\n                  ),\n                  const SizedBox(width: 8),\n                  Expanded(\n                    child: Text('新版本 ${updateInfo.version} 已下载完成'),\n                  ),\n                ],\n              ),\n              const SizedBox(height: 12),\n              Text(\n                '安装过程中应用将会退出',\n                style: TextStyle(\n                  color: Theme.of(context).colorScheme.error,\n                  fontSize: 12,\n                ),\n              ),\n              const SizedBox(height: 12),\n              Container(\n                padding: const EdgeInsets.all(8),\n                decoration: BoxDecoration(\n                  color: Theme.of(context).colorScheme.surfaceContainerHighest,\n                  borderRadius: BorderRadius.circular(4),\n                ),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      '文件位置:',\n                      style: Theme.of(context).textTheme.labelSmall,\n                    ),\n                    const SizedBox(height: 4),\n                    SelectableText(\n                      filePath,\n                      style: Theme.of(context).textTheme.bodySmall?.copyWith(\n                            fontFamily: 'monospace',\n                          ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n          actions: [\n            TextButton(\n              onPressed: () => KazumiDialog.dismiss(),\n              child: Text(\n                '稍后安装',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            if (Utils.isDesktop())\n              TextButton(\n                onPressed: () {\n                  // 在文件管理器中显示文件\n                  _revealInFileManager(filePath);\n                },\n                child: const Text('打开文件夹'),\n              ),\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n                _installUpdate(\n                    filePath, updateInfo.recommendedInstallationType);\n              },\n              child: const Text('立即安装'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  /// 下载文件\n  Future<String> _downloadFile(\n      String url, String version, String expectedHash) async {\n    final fileName = _getFileNameFromUrl(url, version);\n\n    // 统一使用临时目录\n    final tempDir = await getTemporaryDirectory();\n    final filePath = '${tempDir.path}/$fileName';\n    final file = File(filePath);\n\n    // 检查文件是否已存在\n    if (await file.exists()) {\n      try {\n        //使用哈希验证文件完整性\n        final localHash = await Utils.calculateFileHash(file);\n        if (localHash == expectedHash) {\n          // 文件已存在且哈希匹配，直接返回\n          KazumiLogger().i('Update: file already exists and hash verified, skipping download: $filePath');\n          _downloadProgress.value = 1.0;\n          return filePath;\n        } else {\n          // 文件存在但哈希不匹配，删除后重新下载\n          KazumiLogger().i(\n              'Update: file hash mismatch detected (local: $localHash, expected: $expectedHash), deleting and re-downloading');\n          await file.delete();\n        }\n      } catch (e) {\n        // 验证过程中出错，删除文件重新下载\n        KazumiLogger().w('Update: file verification failed, deleting and re-downloading', error: e);\n        if (await file.exists()) {\n          await file.delete();\n        }\n      }\n    }\n\n    _cancelToken = CancelToken();\n\n    await _dio.download(\n      url,\n      filePath,\n      cancelToken: _cancelToken,\n      onReceiveProgress: (received, total) {\n        if (total > 0) {\n          _downloadProgress.value = received / total;\n        }\n      },\n    );\n\n    // 下载完成后验证文件哈希\n    final downloadedHash = await Utils.calculateFileHash(file);\n    if (downloadedHash != expectedHash) {\n      // 哈希不匹配，删除文件并抛出异常\n      await file.delete();\n      throw Exception('文件完整性验证失败: 期望 $expectedHash，实际 $downloadedHash');\n    }\n    KazumiLogger().i('Update: file downloaded and hash verified: $filePath');\n\n    return filePath;\n  }\n\n  /// 安装更新\n  void _installUpdate(\n      String filePath, InstallationType installationType) async {\n    try {\n      // 显示准备退出的提示\n      KazumiDialog.showToast(message: '准备安装更新，应用即将退出...');\n\n      await Future.delayed(const Duration(seconds: 2));\n\n      if (Platform.isWindows) {\n        if (installationType == InstallationType.windowsMsix) {\n          final Uri fileUri = Uri.file(filePath);\n          if (await canLaunchUrl(fileUri)) {\n            await launchUrl(fileUri);\n          } else {\n            throw 'Could not launch $fileUri';\n          }\n        } else {\n          await Process.start('explorer.exe', [filePath], runInShell: true);\n        }\n        await Future.delayed(const Duration(seconds: 1));\n        exit(0);\n      } else if (Platform.isMacOS) {\n        if (filePath.endsWith('.dmg')) {\n          await Process.start('open', [filePath]);\n          exit(0);\n        }\n      } else if (Platform.isAndroid) {\n        final result = await OpenFilex.open(filePath);\n        if (result.type != ResultType.done) {\n          KazumiDialog.showToast(message: '无法打开安装文件: ${result.message}');\n          return;\n        }\n      }\n    } catch (e) {\n      KazumiDialog.showToast(message: '启动安装程序失败: ${e.toString()}');\n      KazumiLogger().e('Update: launch installer failed', error: e);\n    }\n  }\n\n  /// 在文件管理器中显示文件\n  void _revealInFileManager(String filePath) async {\n    try {\n      final type = await FileSystemEntity.type(filePath);\n      String targetDirOrFile;\n\n      // 如果传入的本来就是目录则打开这个目录\n      // 如果是文件则打开包含它的目录\n      if (type == FileSystemEntityType.notFound) {\n        KazumiDialog.showToast(message: '文件或目录不存在');\n        return;\n      } else if (type == FileSystemEntityType.directory) {\n        targetDirOrFile = filePath;\n      } else {\n        targetDirOrFile = File(filePath).parent.path;\n      }\n\n      if (Platform.isWindows) {\n        if (type == FileSystemEntityType.file) {\n          final arg = '/select,${filePath.replaceAll('/', r'\\')}';\n          await Process.start('explorer.exe', [arg], runInShell: true);\n        } else {\n          await Process.start('explorer.exe', [targetDirOrFile.replaceAll('/', r'\\')], runInShell: true);\n        }\n      } else if (Platform.isMacOS) {\n        if (type == FileSystemEntityType.file) {\n          await Process.start('open', ['-R', filePath]);\n        } else {\n          await Process.start('open', [targetDirOrFile]);\n        }\n      } else if (Platform.isLinux) {\n        // 尝试打开包含文件的文件夹\n        await Process.start('xdg-open', [targetDirOrFile]);\n      } else {\n        KazumiDialog.showToast(message: '此平台不支持通过此方法打开文件管理器');\n      }\n    } catch (e) {\n      KazumiDialog.showToast(message: '无法打开文件管理器');\n      KazumiLogger().w('Update: reveal in file manager failed', error: e);\n    } finally {\n      try {\n        // 确保对话框被关闭\n        KazumiDialog.dismiss();\n      } catch (_) {}\n    }\n  }\n\n  /// 根据安装类型获取下载链接\n  Future<String> _getDownloadUrlForType(\n      List<dynamic> assets, InstallationType type) async {\n    final patterns =\n        _getFilePatterns(type).map((p) => p.toLowerCase()).toList();\n\n    try {\n      final asset = assets.cast<Map<String, dynamic>>().firstWhere((asset) {\n        final name = (asset['name'] as String?)?.toLowerCase() ?? '';\n        final downloadUrl = (asset['browser_download_url'] as String?) ?? '';\n        return downloadUrl.isNotEmpty &&\n            patterns.every((pattern) => name.contains(pattern));\n      });\n      return (asset['browser_download_url'] as String?) ?? '';\n    } catch (e) {\n      return '';\n    }\n  }\n\n  /// 获取合适的下载链接\n  /// 根据安装类型获取文件名模式\n  List<String> _getFilePatterns(InstallationType installationType) {\n    switch (installationType) {\n      case InstallationType.windowsMsix:\n        return ['windows', '.msix'];\n      case InstallationType.windowsPortable:\n        return ['windows', '.zip'];\n      case InstallationType.macosDmg:\n        return ['macos', '.dmg'];\n      case InstallationType.androidApk:\n        return ['android', '.apk'];\n      // 以下类型直接跳转到 GitHub Release 页面，不需要下载文件\n      case InstallationType.linuxDeb:\n      case InstallationType.linuxTar:\n      case InstallationType.ios:\n      case InstallationType.unknown:\n        return [];\n    }\n  }\n\n  /// 从URL获取文件名\n  String _getFileNameFromUrl(String url, String version) {\n    final uri = Uri.parse(url);\n    final fileName = uri.pathSegments.last;\n\n    if (fileName.isNotEmpty) {\n      return fileName;\n    }\n\n    // 回退方案\n    String extension = '';\n    if (Platform.isWindows) {\n      extension = '.msix';\n    } else if (Platform.isMacOS) {\n      extension = '.dmg';\n    } else if (Platform.isLinux) {\n      extension = '.deb';\n    } else if (Platform.isAndroid) {\n      extension = '.apk';\n    }\n    return 'Kazumi-$version$extension';\n  }\n\n  /// 从 assets 中获取文件的哈希值\n  String _getFileHashFromAssets(List<dynamic> assets, String downloadUrl) {\n    for (final asset in assets) {\n      final assetDownloadUrl = asset['browser_download_url'] as String? ?? '';\n      if (assetDownloadUrl == downloadUrl) {\n        final digest = asset['digest'] as String? ?? '';\n        if (digest.isNotEmpty && digest.startsWith('sha256:')) {\n          return digest.substring(7); // 移除 \"sha256:\" 前缀\n        }\n      }\n    }\n    return '';\n  }\n}\n"
  },
  {
    "path": "lib/utils/background_download_service.dart",
    "content": "import 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_foreground_task/flutter_foreground_task.dart';\nimport 'package:kazumi/utils/logger.dart';\n\n/// Android 后台下载服务\n///\n/// 使用 Foreground Service 保持 app 进程存活，防止系统在后台时杀死下载进程。\n/// 下载逻辑仍在主 Isolate 运行，此服务仅负责：\n/// 1. 显示通知栏进度\n/// 2. 保持进程存活\n/// 3. 提供通知栏交互（暂停/取消）\nclass BackgroundDownloadService {\n  static final BackgroundDownloadService _instance = BackgroundDownloadService._internal();\n  factory BackgroundDownloadService() => _instance;\n  BackgroundDownloadService._internal();\n\n  bool _isInitialized = false;\n  bool _isRunning = false;\n\n  void Function()? onPauseAll;\n  void Function()? onCancelAll;\n  void Function()? onNavigateToDownloadRequested;\n\n  /// 返回 true 表示用户同意请求权限，false 表示用户拒绝\n  Future<bool> Function()? onNotificationPermissionRequired;\n\n  bool get isSupported => Platform.isAndroid;\n  bool get isRunning => _isRunning;\n  Future<void> init() async {\n    if (!isSupported || _isInitialized) return;\n\n    FlutterForegroundTask.init(\n      androidNotificationOptions: AndroidNotificationOptions(\n        channelId: 'kazumi_download_channel',\n        channelName: '下载服务',\n        channelDescription: '视频下载后台服务',\n        channelImportance: NotificationChannelImportance.LOW,\n        priority: NotificationPriority.LOW,\n        onlyAlertOnce: true,\n      ),\n      iosNotificationOptions: const IOSNotificationOptions(\n        showNotification: false,\n      ),\n      foregroundTaskOptions: ForegroundTaskOptions(\n        eventAction: ForegroundTaskEventAction.nothing(),\n        autoRunOnBoot: false,\n        autoRunOnMyPackageReplaced: false,\n        allowWakeLock: true,\n        allowWifiLock: true,\n      ),\n    );\n\n    FlutterForegroundTask.initCommunicationPort();\n\n    _isInitialized = true;\n    KazumiLogger().i('BackgroundDownloadService: initialized');\n  }\n\n  Future<bool> needsNotificationPermission() async {\n    if (!isSupported) return false;\n    final permission = await FlutterForegroundTask.checkNotificationPermission();\n    return permission != NotificationPermission.granted;\n  }\n\n  Future<bool> requestNotificationPermission() async {\n    if (!isSupported) return true;\n    final result = await FlutterForegroundTask.requestNotificationPermission();\n    return result == NotificationPermission.granted;\n  }\n\n  Future<bool> startService() async {\n    if (!isSupported) return false;\n    if (_isRunning) return true;\n\n    if (!_isInitialized) {\n      await init();\n    }\n\n    final needsPermission = await needsNotificationPermission();\n    if (needsPermission) {\n      if (onNotificationPermissionRequired != null) {\n        final userAgreed = await onNotificationPermissionRequired!();\n        if (userAgreed) {\n          final granted = await requestNotificationPermission();\n          if (!granted) {\n            KazumiLogger().w('BackgroundDownloadService: notification permission denied by user');\n          }\n        } else {\n          KazumiLogger().i('BackgroundDownloadService: user declined permission dialog');\n        }\n      } else {\n        // 没有设置回调，直接请求权限（兼容旧行为）\n        final granted = await requestNotificationPermission();\n        if (!granted) {\n          KazumiLogger().w('BackgroundDownloadService: notification permission denied');\n        }\n      }\n    }\n\n    try {\n      final result = await FlutterForegroundTask.startService(\n        notificationTitle: '正在下载',\n        notificationText: '准备中...',\n        notificationButtons: [\n          const NotificationButton(id: 'pause_all', text: '暂停全部'),\n        ],\n        callback: _backgroundCallback,\n      );\n\n      _isRunning = result is ServiceRequestSuccess;\n\n      if (_isRunning) {\n        KazumiLogger().i('BackgroundDownloadService: service started');\n      } else {\n        KazumiLogger().w('BackgroundDownloadService: service start returned non-success: $result');\n      }\n      return _isRunning;\n    } catch (e) {\n      KazumiLogger().e('BackgroundDownloadService: failed to start service', error: e);\n      return false;\n    }\n  }\n\n  Future<void> stopService() async {\n    if (!isSupported || !_isRunning) return;\n\n    try {\n      await FlutterForegroundTask.stopService();\n      _isRunning = false;\n      KazumiLogger().i('BackgroundDownloadService: service stopped');\n    } catch (e) {\n      KazumiLogger().e('BackgroundDownloadService: failed to stop service', error: e);\n    }\n  }\n\n  Future<void> updateNotification({\n    required String title,\n    required String text,\n  }) async {\n    if (!isSupported || !_isRunning) return;\n\n    try {\n      await FlutterForegroundTask.updateService(\n        notificationTitle: title,\n        notificationText: text,\n      );\n    } catch (e) {\n      // 忽略更新失败，不影响下载\n    }\n  }\n\n  Future<void> updateProgress({\n    required int activeCount,\n    required int totalCount,\n    required double overallProgress,\n    required String speedText,\n  }) async {\n    if (!isSupported || !_isRunning) return;\n\n    String title;\n    String text;\n\n    if (activeCount == 0) {\n      title = '下载已暂停';\n      text = '共 $totalCount 个任务';\n    } else {\n      final percent = (overallProgress * 100).toInt();\n      title = '正在下载 ($activeCount/$totalCount)';\n      text = '$percent% · $speedText';\n    }\n\n    await updateNotification(title: title, text: text);\n  }\n\n  Future<void> showCompletedNotification({\n    required int completedCount,\n  }) async {\n    if (!isSupported) return;\n    await stopService();\n    // TODO: 显示普通通知告知用户下载完成（需要额外的通知插件）\n  }\n\n  void handleNotificationAction(String buttonId) {\n    switch (buttonId) {\n      case 'pause_all':\n        onPauseAll?.call();\n        break;\n      case 'cancel_all':\n        onCancelAll?.call();\n        break;\n    }\n  }\n\n  void handleNavigateToDownload() {\n    onNavigateToDownloadRequested?.call();\n  }\n\n  void addTaskDataCallback(void Function(Object) callback) {\n    FlutterForegroundTask.addTaskDataCallback(callback);\n  }\n\n  void removeTaskDataCallback(void Function(Object) callback) {\n    FlutterForegroundTask.removeTaskDataCallback(callback);\n  }\n}\n\n/// 后台任务回调（在独立 Isolate 中运行）\n///\n/// 注意：此回调主要用于保持服务存活和处理通知交互。\n/// 实际下载逻辑在主 Isolate 中运行。\n@pragma('vm:entry-point')\nvoid _backgroundCallback() {\n  FlutterForegroundTask.setTaskHandler(_DownloadTaskHandler());\n}\n\nclass _DownloadTaskHandler extends TaskHandler {\n  @override\n  Future<void> onStart(DateTime timestamp, TaskStarter starter) async {\n    debugPrint('BackgroundDownloadService: task handler started');\n  }\n\n  @override\n  void onRepeatEvent(DateTime timestamp) {\n    // eventAction 配置为 nothing，不会触发\n  }\n\n  @override\n  void onNotificationButtonPressed(String id) {\n    debugPrint('BackgroundDownloadService: notification button pressed: $id');\n    FlutterForegroundTask.sendDataToMain({'action': 'button_pressed', 'id': id});\n  }\n\n  @override\n  void onNotificationPressed() {\n    FlutterForegroundTask.sendDataToMain({'action': 'navigate_to_download'});\n    FlutterForegroundTask.launchApp();\n  }\n\n  @override\n  void onNotificationDismissed() {\n    // 前台服务通知通常不可划掉\n  }\n\n  @override\n  Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {\n    debugPrint('BackgroundDownloadService: task handler destroyed (isTimeout: $isTimeout)');\n  }\n\n  @override\n  void onReceiveData(Object data) {\n    debugPrint('BackgroundDownloadService: received data: $data');\n  }\n}\n"
  },
  {
    "path": "lib/utils/constants.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:kazumi/request/api.dart';\n\nclass StyleString {\n  static const double cardSpace = 8;\n  static const double safeSpace = 12;\n  static BorderRadius mdRadius = BorderRadius.circular(10);\n  static const Radius imgRadius = Radius.circular(12);\n  static const double aspectRatio = 16 / 10;\n}\n\nconst String customAppFontFamily = \"MI_Sans_Regular\";\n\n/// `year2023` flag is deprecated since 3.29 but not default to false yet. Keep\n/// it to false so we have the latest M3 style process indicator.\n/// ignore: deprecated_member_use\nconst ProgressIndicatorThemeData progressIndicatorTheme2024 =\n    ProgressIndicatorThemeData(year2023: false);\n\n/// `year2023` flag is deprecated since 3.29 but not default to false yet. Keep\n/// it to false so we have the latest M3 style slider.\n/// ignore: deprecated_member_use\nconst SliderThemeData sliderTheme2024 = SliderThemeData(\n  year2023: false,\n  showValueIndicator: ShowValueIndicator.always,\n);\n\n/// The page transition method defined here is managed by flutter, and the native transition method of flutter is set here.\n/// Transition method here will be overridden by the transition method of modular, and do not set the transition method in modular to prevent\n/// the native transition method from failing\nconst PageTransitionsTheme pageTransitionsTheme2024 = PageTransitionsTheme(\n  builders: {\n    TargetPlatform.android: CupertinoPageTransitionsBuilder(),\n    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),\n    TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),\n    TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),\n    TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),\n  },\n);\n\n/// Layout breakpoint according to google:\n/// https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes.\n///\n/// **It's only a suggestion since not every device meet the breakpoint requirement.\n/// You need to build layout with some more judgements.**\n///\n/// Some example device(portrait) width x height:\n///\n/// * iPhone SE3: 375 x 667\n/// * iPhone 16: 393 x 852\n/// * iPad Pro 11-inch: 834 x 1210\n/// * HW MATE60 Pro: 387.7 x 836.9\n/// * OHOS in floating window: 387.7 x 631.7 or 218.1\nclass LayoutBreakpoint {\n  static const Map<String, double> compact = {'width': 600, 'height': 480};\n  static const Map<String, double> medium = {'width': 840, 'height': 900};\n}\n\n/// 随机UA列表\nconst List<String> userAgentsList = [\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0',\n  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.1',\n  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0',\n  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0',\n];\n\n/// 默认 SyncPlay 服务器列表\nconst List<String> defaultSyncPlayEndPoints = [\n  'syncplay.pl:8995',\n  'syncplay.pl:8996',\n  'syncplay.pl:8997',\n  'syncplay.pl:8998',\n  'syncplay.pl:8999',\n];\n\nconst String defaultSyncPlayEndPoint = 'syncplay.pl:8996';\n\n/// 随机HTTP请求头accept-language字段列表\nconst List<String> acceptLanguageList = [\n  'zh-CN,zh;q=0.9',\n  'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',\n  'zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6',\n];\n\n/// Bangumi API 文档要求的UA格式\nMap<String, String> bangumiHTTPHeader = {\n  'user-agent':\n      'Predidit/Kazumi/${Api.version} (Android) (https://github.com/Predidit/Kazumi)',\n  'referer': '',\n  'content-type': 'application/json'\n};\n\n/// 可选硬件解码器\nconst Map<String, String> hardwareDecodersList = {\n  'auto': '启用任意可用解码器',\n  'auto-safe': '启用最佳解码器',\n  'auto-copy': '启用带拷贝功能的最佳解码器',\n  'd3d11va': 'DirectX11 (windows8 及以上)',\n  'd3d11va-copy': 'DirectX11 (windows8 及以上) (非直通)',\n  'videotoolbox': 'VideoToolbox (macOS / iOS)',\n  'videotoolbox-copy': 'VideoToolbox (macOS / iOS) (非直通)',\n  'vaapi': 'VAAPI (Linux)',\n  'vaapi-copy': 'VAAPI (Linux) (非直通)',\n  'nvdec': 'NVDEC (NVIDIA独占)',\n  'nvdec-copy': 'NVDEC (NVIDIA独占) (非直通)',\n  'drm': 'DRM (Linux)',\n  'drm-copy': 'DRM (Linux) (非直通)',\n  'vulkan': 'Vulkan (全平台) (实验性)',\n  'vulkan-copy': 'Vulkan (全平台) (实验性) (非直通)',\n  'dxva2': 'DXVA2 (Windows7 及以上)',\n  'dxva2-copy': 'DXVA2 (Windows7 及以上) (非直通)',\n  'vdpau': 'VDPAU (Linux)',\n  'vdpau-copy': 'VDPAU (Linux) (非直通)',\n  'mediacodec': 'MediaCodec (Android)',\n  'mediacodec-copy': 'MediaCodec (Android) (非直通)',\n  'cuda': 'CUDA (NVIDIA独占) (过时)',\n  'cuda-copy': 'CUDA (NVIDIA独占) (过时) (非直通)',\n  'crystalhd': 'CrystalHD (全平台) (过时)',\n  'rkmpp': 'Rockchip MPP (仅部分Rockchip芯片)',\n};\n\n/// Android 可选视频渲染器\nconst Map<String, String> androidVideoRenderersList = {\n  'auto': '自动选择',\n  'gpu': '基于 OpenGL, 通用和稳健的选项',\n  'gpu-next': '基于 Vulkan, 在新设备上表现最好',\n  'mediacodec_embed': '功耗最低，不支持超分辨率',\n};\n\n/// 超分辨率滤镜\nconst List<String> mpvAnime4KShaders = [\n  'Anime4K_Clamp_Highlights.glsl',\n  'Anime4K_Restore_CNN_VL.glsl',\n  'Anime4K_Upscale_CNN_x2_VL.glsl',\n  'Anime4K_AutoDownscalePre_x2.glsl',\n  'Anime4K_AutoDownscalePre_x4.glsl',\n  'Anime4K_Upscale_CNN_x2_M.glsl'\n];\n\n/// 超分辨率滤镜 (轻量)\nconst List<String> mpvAnime4KShadersLite = [\n  'Anime4K_Clamp_Highlights.glsl',\n  'Anime4K_Restore_CNN_M.glsl',\n  'Anime4K_Restore_CNN_S.glsl',\n  'Anime4K_Upscale_CNN_x2_M.glsl',\n  'Anime4K_AutoDownscalePre_x2.glsl',\n  'Anime4K_AutoDownscalePre_x4.glsl',\n  'Anime4K_Upscale_CNN_x2_S.glsl'\n];\n\n/// 可选播放倍速\nconst List<double> defaultPlaySpeedList = [\n  0.25,\n  0.5,\n  0.75,\n  1.0,\n  1.25,\n  1.5,\n  1.75,\n  2.0,\n  2.25,\n  2.5,\n  2.75,\n  3.0,\n];\n\nconst String danmakuOnSvg = '''\n    <svg xmlns=\"http://www.w3.org/2000/svg\" data-pointer=\"none\" viewBox=\"0 0 24 24\">\n      <path fill=\"#FFFFFF\" fill-rule=\"evenodd\" d=\"M11.989 4.828c-.47 0-.975.004-1.515.012l-1.71-2.566a1.008 1.008 0 0 0-1.678 1.118l.999 1.5c-.681.018-1.403.04-2.164.068a4.013 4.013 0 0 0-3.83 3.44c-.165 1.15-.245 2.545-.245 4.185 0 1.965.115 3.67.35 5.116a4.012 4.012 0 0 0 3.763 3.363l.906.046c1.205.063 1.808.095 3.607.095a.988.988 0 0 0 0-1.975c-1.758 0-2.339-.03-3.501-.092l-.915-.047a2.037 2.037 0 0 1-1.91-1.708c-.216-1.324-.325-2.924-.325-4.798 0-1.563.076-2.864.225-3.904.14-.977.96-1.713 1.945-1.747 2.444-.087 4.465-.13 6.063-.131 1.598 0 3.62.044 6.064.13.96.034 1.71.81 1.855 1.814.075.524.113 1.962.141 3.065v.002c.01.342.017.65.025.88a.987.987 0 1 0 1.974-.068c-.008-.226-.016-.523-.025-.856v-.027c-.03-1.118-.073-2.663-.16-3.276-.273-1.906-1.783-3.438-3.74-3.507-.9-.032-1.743-.058-2.531-.078l1.05-1.46a1.008 1.008 0 0 0-1.638-1.177l-1.862 2.59c-.38-.004-.744-.007-1.088-.007h-.13Zm.521 4.775h-1.32v4.631h2.222v.847h-2.618v1.078h2.618l.003.678c.36.026.714.163 1.01.407h.11v-1.085h2.694v-1.078h-2.695v-.847H16.8v-4.63h-1.276a8.59 8.59 0 0 0 .748-1.42L15.183 7.8a14.232 14.232 0 0 1-.814 1.804h-1.518l.693-.308a8.862 8.862 0 0 0-.814-1.408l-1.045.352c.297.396.572.847.825 1.364Zm-4.18 3.564.154-1.485h1.98V8.294h-3.2v.98H9.33v1.43H7.472l-.308 3.453h2.277c0 1.166-.044 1.925-.12 2.277-.078.352-.386.528-.936.528-.308 0-.616-.022-.902-.055l.297 1.067.062.005c.285.02.551.04.818.04 1.001-.067 1.562-.419 1.694-1.057.11-.638.176-1.903.176-3.795h-2.2Zm7.458.11v-.858h-1.254v.858h1.254Zm-2.376-.858v.858h-1.199v-.858h1.2Zm-1.199-.946h1.2v-.902h-1.2v.902Zm2.321 0v-.902h1.254v.902h-1.254Z\" clip-rule=\"evenodd\"/>\n      <path fill=\"#00AEEC\" fill-rule=\"evenodd\" d=\"M22.846 14.627a1 1 0 0 0-1.412.075l-5.091 5.703-2.216-2.275-.097-.086-.008-.005a1 1 0 0 0-1.322 1.493l2.963 3.041.093.083.007.005a1 1 0 0 0 1.354-.124l5.81-6.505.08-.102.005-.008a1 1 0 0 0-.166-1.295Z\" clip-rule=\"evenodd\"/>\n    </svg>\n    ''';\n\n/// 可选默认视频比例\nconst Map<int, String> aspectRatioTypeMap = {\n  1: \"自动\",\n  2: \"裁切填充\",\n  3: \"拉伸填充\",\n};\n\n/// 可选播放器日志等级\n/// LogLevel 0: 错误 1: 警告 2: 简略 3: 详细 4: 调试（隐藏） 5: 全部（隐藏）\nconst Map<int, String> playerLogLevelMap = {\n  0: \"错误\",\n  1: \"警告\",\n  2: \"简略\",\n  3: \"详细\",\n  // 以下两个级别被MPV官方支持，但是输出内容过于冗长，暂时隐藏\n  // 4: \"调试\",\n  // 5: \"全部\",\n};\n\nfinal List<String> defaultAnimeTags = const [\n  '日常',\n  '原创',\n  '校园',\n  '搞笑',\n  '奇幻',\n  '百合',\n  '恋爱',\n  '悬疑',\n  '热血',\n  '后宫',\n  '机战',\n  '轻改',\n  '偶像',\n  '治愈',\n  '异世界',\n];\n\n// 播放器默认快捷键\n  final Map<String, List<String>> defaultShortcuts = const {\n    'playorpause': [' '],\n    'forward': ['Arrow Right'],\n    'rewind': ['Arrow Left'],\n    'next': ['N'],\n    'prev': ['P'],\n    'volumeup': ['Arrow Up'],\n    'volumedown': ['Arrow Down'],\n    'togglemute': ['M'],\n    'fullscreen': ['F'],\n    'exitfullscreen': ['Escape'],\n    'toggledanmaku': ['D'],\n    'screenshot': ['S'],\n    'skip': ['K'],\n    'speed1': ['1'],\n    'speed2': ['2'],\n    'speed3': ['3'],\n    'speedup': ['X'],\n    'speeddown': ['Z'],\n  };\n\n// 键位别名\n  final Map<String, String> keyAliases = {\n    ' ': '空格',\n    'Arrow Up': '↑',\n    'Arrow Down': '↓',\n    'Arrow Left': '←',\n    'Arrow Right': '→',\n    'Enter': '回车',\n    'Tab': 'Tab',\n    'Escape': 'Esc',\n    'Backspace': '退格',\n  };\n\n//功能中文名对应\n  final Map<String, String> shortcutsChineseName = {\n    'playorpause': '播放 / 暂停',\n    'forward': '快进 / 长按倍速',\n    'rewind': '快退',\n    'next': '下一集',\n    'prev': '上一集',\n    'volumeup': '音量加',\n    'volumedown': '音量减',\n    'togglemute': '静音',\n    'fullscreen': '全屏',\n    'exitfullscreen': '退出全屏',\n    'toggledanmaku': '弹幕开关',\n    'screenshot': '截图',\n    'skip': '跳过',\n    'speed1': '倍速：1x',\n    'speed2': '倍速：2x',\n    'speed3': '倍速：3x',\n    'speedup': '倍速加',\n    'speeddown': '倍速减',\n  };"
  },
  {
    "path": "lib/utils/download_manager.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'package:dio/dio.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\nimport 'package:kazumi/utils/m3u8_parser.dart';\nimport 'package:kazumi/utils/m3u8_ad_filter.dart';\nimport 'package:kazumi/utils/format_utils.dart' as fmt;\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:path_provider/path_provider.dart';\n\nclass _NotM3u8Exception implements Exception {\n  final String message;\n  _NotM3u8Exception(this.message);\n  @override\n  String toString() => message;\n}\n\nclass _InsufficientStorageException implements Exception {\n  final int availableBytes;\n  final int requiredBytes;\n  _InsufficientStorageException(this.availableBytes, this.requiredBytes);\n  @override\n  String toString() => '存储空间不足';\n}\n\nclass DownloadTask {\n  final String recordKey;\n  final int episodeNumber;\n  CancelToken cancelToken;\n  bool isPaused;\n\n  DownloadTask({\n    required this.recordKey,\n    required this.episodeNumber,\n    CancelToken? cancelToken,\n    this.isPaused = false,\n  }) : cancelToken = cancelToken ?? CancelToken();\n}\n\ntypedef ProgressCallback = void Function(\n    String recordKey, int episodeNumber, DownloadEpisode episode, double speed);\n\nclass DownloadRequest {\n  final String recordKey;\n  final int bangumiId;\n  final String pluginName;\n  final int episodeNumber;\n  final String m3u8Url;\n  final Map<String, String> httpHeaders;\n  final bool adBlockerEnabled;\n  final DownloadEpisode episode;\n\n  const DownloadRequest({\n    required this.recordKey,\n    required this.bangumiId,\n    required this.pluginName,\n    required this.episodeNumber,\n    required this.m3u8Url,\n    required this.httpHeaders,\n    required this.adBlockerEnabled,\n    required this.episode,\n  });\n}\n\nabstract class IDownloadManager {\n  ProgressCallback? onProgress;\n\n  bool isDownloading(String recordKey, int episodeNumber);\n  Future<void> enqueue(DownloadRequest request);\n  Future<void> enqueuePriority(DownloadRequest request);\n  void pause(String recordKey, int episodeNumber);\n  Future<void> resume(DownloadRequest request);\n  void cancel(String recordKey, int episodeNumber);\n  String? getLocalVideoPath(DownloadEpisode? episode);\n  Future<void> deleteEpisodeFiles(int bangumiId, String pluginName, int episodeNumber);\n  Future<void> deleteRecordFiles(int bangumiId, String pluginName);\n  double getSpeed(String recordKey, int episodeNumber);\n}\n\nclass _SpeedTracker {\n  int _lastBytes = 0;\n  DateTime _lastTime = DateTime.now();\n  double currentSpeed = 0.0; // bytes/sec\n\n  void update(int totalBytes) {\n    final now = DateTime.now();\n    final elapsed = now.difference(_lastTime).inMilliseconds;\n    if (elapsed > 500) {\n      final bytesDownloaded = totalBytes - _lastBytes;\n      currentSpeed = bytesDownloaded / (elapsed / 1000);\n      _lastBytes = totalBytes;\n      _lastTime = now;\n    }\n  }\n\n  void reset() {\n    _lastBytes = 0;\n    _lastTime = DateTime.now();\n    currentSpeed = 0.0;\n  }\n}\n\nclass DownloadManager implements IDownloadManager {\n  DownloadManager() {\n    _loadSettings();\n  }\n\n  final Dio _dio = Dio(BaseOptions(\n    connectTimeout: const Duration(seconds: 15),\n    receiveTimeout: const Duration(seconds: 30),\n  ));\n\n  final Map<String, DownloadTask> _activeTasks = {};\n  final List<DownloadRequest> _queue = [];\n  final Map<String, _SpeedTracker> _speedTrackers = {};\n  int maxParallelEpisodes = 2;\n  int maxParallelSegments = 3;\n  int _runningCount = 0;\n\n  @override\n  ProgressCallback? onProgress;\n\n  static const _minRequiredSpace = 100 * 1024 * 1024; // 100MB minimum\n  static const _storageChannel = MethodChannel('com.predidit.kazumi/storage');\n\n  void _loadSettings() {\n    final setting = GStorage.setting;\n    maxParallelEpisodes = setting.get(\n      SettingBoxKey.downloadParallelEpisodes,\n      defaultValue: 2,\n    );\n    maxParallelSegments = setting.get(\n      SettingBoxKey.downloadParallelSegments,\n      defaultValue: 3,\n    );\n  }\n\n  /// Returns available bytes, or -1 if unable to determine\n  Future<int> _getAvailableStorage(String path) async {\n    try {\n      final result = await _storageChannel.invokeMethod<int>(\n        'getAvailableStorage',\n        {'path': path},\n      );\n      return result ?? -1;\n    } on MissingPluginException {\n      return -1;\n    } catch (e) {\n      KazumiLogger().w('DownloadManager: failed to get storage info', error: e);\n      return -1;\n    }\n  }\n\n  Future<void> _checkStorageSpace(String downloadDir, {int requiredBytes = 0}) async {\n    final available = await _getAvailableStorage(downloadDir);\n    if (available == -1) return; // Skip check if unable to determine\n\n    final required = requiredBytes > 0 ? requiredBytes : _minRequiredSpace;\n    if (available < required) {\n      throw _InsufficientStorageException(available, required);\n    }\n  }\n\n  String _getStorageErrorMessage(FileSystemException e) {\n    // POSIX error code 28 = ENOSPC (No space left on device)\n    if (e.osError?.errorCode == 28) {\n      return '存储空间不足，请清理后重试';\n    }\n    // POSIX error code 13 = EACCES (Permission denied)\n    if (e.osError?.errorCode == 13) {\n      return '存储权限被拒绝';\n    }\n    // POSIX error code 30 = EROFS (Read-only file system)\n    if (e.osError?.errorCode == 30) {\n      return '存储为只读，无法写入';\n    }\n    return '存储错误: ${e.message}';\n  }\n\n  @override\n  double getSpeed(String recordKey, int episodeNumber) {\n    final key = _taskKey(recordKey, episodeNumber);\n    return _speedTrackers[key]?.currentSpeed ?? 0.0;\n  }\n\n  String _taskKey(String recordKey, int episodeNumber) =>\n      '${recordKey}_$episodeNumber';\n\n  @override\n  bool isDownloading(String recordKey, int episodeNumber) =>\n      _activeTasks.containsKey(_taskKey(recordKey, episodeNumber));\n\n  Future<String> get _downloadBaseDir async {\n    final appSupport = await getApplicationSupportDirectory();\n    return '${appSupport.path}/downloads';\n  }\n\n  String getEpisodeDir(String downloadBase, int bangumiId, String pluginName, int episodeNumber) {\n    return '$downloadBase/${bangumiId}_$pluginName/$episodeNumber';\n  }\n\n  @override\n  Future<void> enqueue(DownloadRequest request) async {\n    final key = _taskKey(request.recordKey, request.episodeNumber);\n    if (_activeTasks.containsKey(key)) return;\n\n    final task = DownloadTask(\n      recordKey: request.recordKey,\n      episodeNumber: request.episodeNumber,\n    );\n\n    if (_runningCount < maxParallelEpisodes) {\n      _runningCount++;\n      _activeTasks[key] = task;\n      _runEpisodeDownload(\n        task: task,\n        bangumiId: request.bangumiId,\n        pluginName: request.pluginName,\n        m3u8Url: request.m3u8Url,\n        httpHeaders: request.httpHeaders,\n        adBlockerEnabled: request.adBlockerEnabled,\n        episode: request.episode,\n      );\n    } else {\n      request.episode.status = DownloadStatus.pending;\n      _queue.add(request);\n      _activeTasks[key] = task;\n    }\n  }\n\n  @override\n  Future<void> enqueuePriority(DownloadRequest request) async {\n    final key = _taskKey(request.recordKey, request.episodeNumber);\n\n    _queue.removeWhere(\n      (r) => r.recordKey == request.recordKey && r.episodeNumber == request.episodeNumber,\n    );\n    _activeTasks.remove(key);\n\n    final task = DownloadTask(\n      recordKey: request.recordKey,\n      episodeNumber: request.episodeNumber,\n    );\n\n    // Start immediately, bypassing the parallel limit (priority download)\n    _runningCount++;\n    _activeTasks[key] = task;\n    _runEpisodeDownload(\n      task: task,\n      bangumiId: request.bangumiId,\n      pluginName: request.pluginName,\n      m3u8Url: request.m3u8Url,\n      httpHeaders: request.httpHeaders,\n      adBlockerEnabled: request.adBlockerEnabled,\n      episode: request.episode,\n    );\n  }\n\n  @override\n  void pause(String recordKey, int episodeNumber) {\n    final key = _taskKey(recordKey, episodeNumber);\n    final task = _activeTasks[key];\n    if (task != null) {\n      task.isPaused = true;\n      task.cancelToken.cancel('paused');\n    }\n  }\n\n  @override\n  Future<void> resume(DownloadRequest request) async {\n    final key = _taskKey(request.recordKey, request.episodeNumber);\n    _activeTasks.remove(key);\n\n    final task = DownloadTask(\n      recordKey: request.recordKey,\n      episodeNumber: request.episodeNumber,\n    );\n    _activeTasks[key] = task;\n\n    if (_runningCount < maxParallelEpisodes) {\n      _runningCount++;\n      _runEpisodeDownload(\n        task: task,\n        bangumiId: request.bangumiId,\n        pluginName: request.pluginName,\n        m3u8Url: request.m3u8Url,\n        httpHeaders: request.httpHeaders,\n        adBlockerEnabled: request.adBlockerEnabled,\n        episode: request.episode,\n      );\n    } else {\n      _queue.add(request);\n    }\n  }\n\n  @override\n  void cancel(String recordKey, int episodeNumber) {\n    final key = _taskKey(recordKey, episodeNumber);\n    final task = _activeTasks[key];\n    if (task != null) {\n      task.cancelToken.cancel('cancelled');\n      _activeTasks.remove(key);\n      _queue.removeWhere(\n        (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber,\n      );\n    }\n  }\n\n  void _processQueue() {\n    while (_runningCount < maxParallelEpisodes && _queue.isNotEmpty) {\n      final request = _queue.removeAt(0);\n      final key = _taskKey(request.recordKey, request.episodeNumber);\n      final existingTask = _activeTasks[key];\n      if (existingTask == null || existingTask.isPaused || existingTask.cancelToken.isCancelled) {\n        _activeTasks.remove(key);\n        continue;\n      }\n\n      _runningCount++;\n      _runEpisodeDownload(\n        task: existingTask,\n        bangumiId: request.bangumiId,\n        pluginName: request.pluginName,\n        m3u8Url: request.m3u8Url,\n        httpHeaders: request.httpHeaders,\n        adBlockerEnabled: request.adBlockerEnabled,\n        episode: request.episode,\n      );\n    }\n  }\n\n  Future<void> _runEpisodeDownload({\n    required DownloadTask task,\n    required int bangumiId,\n    required String pluginName,\n    required String m3u8Url,\n    required Map<String, String> httpHeaders,\n    required bool adBlockerEnabled,\n    required DownloadEpisode episode,\n  }) async {\n    final key = _taskKey(task.recordKey, task.episodeNumber);\n    try {\n      episode.status = DownloadStatus.downloading;\n      episode.networkM3u8Url = m3u8Url;\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n\n      String m3u8Content;\n      try {\n        m3u8Content = await _fetchM3u8(m3u8Url, httpHeaders, task.cancelToken);\n      } on _NotM3u8Exception {\n        KazumiLogger().i(\n          'DownloadManager: URL is not M3U8, falling back to direct file download '\n          'for episode ${task.episodeNumber}',\n        );\n        await _runDirectFileDownload(\n          task: task,\n          bangumiId: bangumiId,\n          pluginName: pluginName,\n          videoUrl: m3u8Url,\n          httpHeaders: httpHeaders,\n          episode: episode,\n        );\n        return;\n      }\n\n      final type = M3u8Parser.detectType(m3u8Content);\n      String mediaM3u8Content = m3u8Content;\n      String mediaM3u8Url = m3u8Url;\n\n      if (type == M3u8Type.master) {\n        final master = M3u8Parser.parseMasterPlaylist(m3u8Content, m3u8Url);\n        final bestVariant = master.bestVariant;\n        mediaM3u8Url = bestVariant.uri;\n        mediaM3u8Content = await _fetchM3u8(mediaM3u8Url, httpHeaders, task.cancelToken);\n      }\n\n      final playlist = M3u8Parser.parseMediaPlaylist(mediaM3u8Content, mediaM3u8Url);\n\n      // 展开嵌套 m3u8 片段（部分源将实际内容嵌套在 m3u8 引用中）\n      final resolvedSegments = await M3u8Parser.resolveNestedSegments(\n        playlist.segments,\n        (url) => _fetchM3u8(url, httpHeaders, task.cancelToken),\n      );\n      final resolvedPlaylist = M3u8MediaPlaylist(\n        segments: resolvedSegments,\n        targetDuration: playlist.targetDuration,\n        isVod: playlist.isVod,\n      );\n\n      if (!resolvedPlaylist.isVod) {\n        episode.status = DownloadStatus.failed;\n        episode.errorMessage = '不支持下载直播流 (无有效分片)';\n        _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        _onTaskComplete(key);\n        return;\n      }\n\n      if (resolvedPlaylist.segments.isEmpty) {\n        episode.status = DownloadStatus.failed;\n        episode.errorMessage = 'M3U8 中未找到可下载的分片';\n        _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        _onTaskComplete(key);\n        return;\n      }\n\n      List<M3u8Segment> segments = resolvedPlaylist.segments;\n      if (adBlockerEnabled) {\n        segments = M3u8AdFilter.filterAds(segments);\n      }\n\n      final base = await _downloadBaseDir;\n      final episodeDir = getEpisodeDir(base, bangumiId, pluginName, task.episodeNumber);\n      await Directory(episodeDir).create(recursive: true);\n      episode.downloadDirectory = episodeDir;\n      await _checkStorageSpace(base);\n      final keys = M3u8Parser.extractUniqueKeys(\n        M3u8MediaPlaylist(\n          segments: segments,\n          targetDuration: resolvedPlaylist.targetDuration,\n          isVod: true,\n        ),\n      );\n      final keyUriToLocal = <String, String>{};\n      for (int i = 0; i < keys.length; i++) {\n        final keyFile = 'key_$i.key';\n        final keyPath = '$episodeDir/$keyFile';\n        await _downloadFile(keys[i].uri, keyPath, httpHeaders, task.cancelToken);\n        keyUriToLocal[keys[i].uri] = keyFile;\n      }\n\n      episode.totalSegments = segments.length;\n      episode.downloadedSegments = 0;\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      final episodeDirObj = Directory(episodeDir);\n      if (await episodeDirObj.exists()) {\n        await for (final entity in episodeDirObj.list()) {\n          if (entity.path.endsWith('.tmp')) {\n            try {\n              await entity.delete();\n            } catch (_) {}\n          }\n        }\n      }\n\n      final existingSegments = <int>{};\n      for (int i = 0; i < segments.length; i++) {\n        final segFile = File('$episodeDir/seg_${i.toString().padLeft(5, '0')}.ts');\n        if (await segFile.exists() && await segFile.length() > 0) {\n          existingSegments.add(i);\n          episode.downloadedSegments++;\n        }\n      }\n\n      final pendingIndices = <int>[];\n      for (int i = 0; i < segments.length; i++) {\n        if (!existingSegments.contains(i)) {\n          pendingIndices.add(i);\n        }\n      }\n\n      int totalBytes = 0;\n      final completer = Completer<void>();\n      int completedCount = 0;\n      int failedCount = 0;\n      final semaphore = _Semaphore(maxParallelSegments);\n\n      _speedTrackers[key] = _SpeedTracker();\n\n      if (pendingIndices.isEmpty) {\n      } else {\n        for (final idx in pendingIndices) {\n          if (task.isPaused || task.cancelToken.isCancelled) break;\n\n          await semaphore.acquire();\n          if (task.isPaused || task.cancelToken.isCancelled) {\n            semaphore.release();\n            break;\n          }\n\n          _downloadSegmentWithRetry(\n            segments[idx].uri,\n            '$episodeDir/seg_${idx.toString().padLeft(5, '0')}.ts',\n            httpHeaders,\n            task.cancelToken,\n          ).then((bytes) {\n            totalBytes += bytes;\n            episode.downloadedSegments++;\n            episode.totalBytes = totalBytes;\n            episode.progressPercent =\n                episode.downloadedSegments / episode.totalSegments;\n            _speedTrackers[key]?.update(totalBytes);\n            _notifyProgress(task.recordKey, task.episodeNumber, episode);\n            completedCount++;\n            semaphore.release();\n            if (completedCount + failedCount == pendingIndices.length) {\n              completer.complete();\n            }\n          }).catchError((e) {\n            failedCount++;\n            semaphore.release();\n            if (completedCount + failedCount == pendingIndices.length) {\n              completer.complete();\n            }\n          });\n        }\n\n        if (!task.isPaused && !task.cancelToken.isCancelled && pendingIndices.isNotEmpty) {\n          await completer.future;\n        }\n      }\n\n      if (task.isPaused || task.cancelToken.isCancelled) {\n        if (task.isPaused) {\n          episode.status = DownloadStatus.paused;\n        }\n        _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        _onTaskComplete(key);\n        return;\n      }\n\n      if (failedCount > 0) {\n        episode.status = DownloadStatus.failed;\n        episode.errorMessage = '$failedCount 个分片下载失败';\n        _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        _onTaskComplete(key);\n        return;\n      }\n\n      final targetDuration = adBlockerEnabled\n          ? M3u8AdFilter.calculateTargetDuration(segments)\n          : resolvedPlaylist.targetDuration;\n      final localM3u8 = M3u8Parser.buildLocalM3u8(\n        segments,\n        targetDuration: targetDuration,\n        keyUriToLocal: keyUriToLocal,\n      );\n      final m3u8Path = '$episodeDir/playlist.m3u8';\n      await File(m3u8Path).writeAsString(localM3u8);\n\n      episode.status = DownloadStatus.completed;\n      episode.localM3u8Path = m3u8Path;\n      episode.progressPercent = 1.0;\n      episode.completedAt = DateTime.now();\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n\n      KazumiLogger().i(\n        'DownloadManager: episode ${task.episodeNumber} completed. '\n        '${segments.length} segments, ${(totalBytes / 1024 / 1024).toStringAsFixed(1)} MB',\n      );\n    } on _InsufficientStorageException catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})';\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().w('DownloadManager: insufficient storage space', error: e);\n    } on FileSystemException catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = _getStorageErrorMessage(e);\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().e('DownloadManager: file system error', error: e);\n    } on DioException catch (e) {\n      if (e.type == DioExceptionType.cancel) {\n        if (task.isPaused) {\n          episode.status = DownloadStatus.paused;\n        }\n      } else {\n        episode.status = DownloadStatus.failed;\n        episode.errorMessage = e.message ?? '网络错误';\n      }\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n    } catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = e.toString();\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().e('DownloadManager: episode download failed', error: e);\n    } finally {\n      _onTaskComplete(key);\n    }\n  }\n\n  Future<void> _runDirectFileDownload({\n    required DownloadTask task,\n    required int bangumiId,\n    required String pluginName,\n    required String videoUrl,\n    required Map<String, String> httpHeaders,\n    required DownloadEpisode episode,\n  }) async {\n    final key = _taskKey(task.recordKey, task.episodeNumber);\n    try {\n      final base = await _downloadBaseDir;\n      final episodeDir = getEpisodeDir(base, bangumiId, pluginName, task.episodeNumber);\n      await Directory(episodeDir).create(recursive: true);\n      episode.downloadDirectory = episodeDir;\n      await _checkStorageSpace(base);\n\n      final filePath = '$episodeDir/video.mp4';\n      final tmpPath = '$filePath.tmp';\n\n      int existingBytes = 0;\n      final tmpFile = File(tmpPath);\n      if (await tmpFile.exists()) {\n        existingBytes = await tmpFile.length();\n      }\n\n      episode.totalSegments = 1;\n      episode.downloadedSegments = 0;\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n\n      final requestHeaders = Map<String, String>.from(httpHeaders);\n      bool useRange = existingBytes > 0;\n      if (useRange) {\n        requestHeaders['Range'] = 'bytes=$existingBytes-';\n      }\n\n      Response<ResponseBody> response;\n      try {\n        response = await _dio.get<ResponseBody>(\n          videoUrl,\n          options: Options(\n            headers: requestHeaders,\n            responseType: ResponseType.stream,\n            receiveTimeout: const Duration(minutes: 30),\n          ),\n          cancelToken: task.cancelToken,\n        );\n      } on DioException catch (e) {\n        if (e.response?.statusCode == 416 && useRange) {\n          KazumiLogger().w(\n            'DownloadManager: 416 Range Not Satisfiable, deleting tmp file and retrying',\n          );\n          await tmpFile.delete();\n          existingBytes = 0;\n          requestHeaders.remove('Range');\n          response = await _dio.get<ResponseBody>(\n            videoUrl,\n            options: Options(\n              headers: requestHeaders,\n              responseType: ResponseType.stream,\n              receiveTimeout: const Duration(minutes: 30),\n            ),\n            cancelToken: task.cancelToken,\n          );\n        } else {\n          rethrow;\n        }\n      }\n\n      final contentRange = response.headers.value('content-range');\n      final contentLength = int.tryParse(\n          response.headers.value(Headers.contentLengthHeader) ?? '') ?? 0;\n      int totalSize;\n      if (contentRange != null) {\n        final totalMatch = RegExp(r'/(\\d+)').firstMatch(contentRange);\n        totalSize = totalMatch != null ? int.parse(totalMatch.group(1)!) : 0;\n      } else {\n        totalSize = existingBytes + contentLength;\n      }\n\n      final raf = await tmpFile.open(\n          mode: existingBytes > 0 ? FileMode.append : FileMode.write);\n      int received = existingBytes;\n\n      _speedTrackers[key] = _SpeedTracker();\n\n      try {\n        await for (final chunk in response.data!.stream) {\n          if (task.isPaused || task.cancelToken.isCancelled) break;\n          await raf.writeFrom(chunk);\n          received += chunk.length;\n          episode.totalBytes = received;\n          episode.progressPercent = totalSize > 0 ? received / totalSize : 0;\n          // Update speed tracker\n          _speedTrackers[key]?.update(received);\n          _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        }\n      } finally {\n        await raf.close();\n      }\n\n      if (task.isPaused || task.cancelToken.isCancelled) {\n        if (task.isPaused) {\n          episode.status = DownloadStatus.paused;\n        }\n        _notifyProgress(task.recordKey, task.episodeNumber, episode);\n        _onTaskComplete(key);\n        return;\n      }\n\n      await File(tmpPath).rename(filePath);\n\n      episode.status = DownloadStatus.completed;\n      episode.localM3u8Path = filePath;\n      episode.downloadedSegments = 1;\n      episode.progressPercent = 1.0;\n      episode.completedAt = DateTime.now();\n      episode.totalBytes = await File(filePath).length();\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n\n      KazumiLogger().i(\n        'DownloadManager: episode ${task.episodeNumber} completed (direct download). '\n        '${(episode.totalBytes / 1024 / 1024).toStringAsFixed(1)} MB',\n      );\n    } on _InsufficientStorageException catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})';\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().w('DownloadManager: insufficient storage space', error: e);\n    } on FileSystemException catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = _getStorageErrorMessage(e);\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().e('DownloadManager: file system error', error: e);\n    } on DioException catch (e) {\n      if (e.type == DioExceptionType.cancel) {\n        if (task.isPaused) {\n          episode.status = DownloadStatus.paused;\n        }\n      } else {\n        episode.status = DownloadStatus.failed;\n        episode.errorMessage = e.message ?? '网络错误';\n      }\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n    } catch (e) {\n      episode.status = DownloadStatus.failed;\n      episode.errorMessage = e.toString();\n      _notifyProgress(task.recordKey, task.episodeNumber, episode);\n      KazumiLogger().e('DownloadManager: direct file download failed', error: e);\n    } finally {\n      _onTaskComplete(key);\n    }\n  }\n\n  void _onTaskComplete(String key) {\n    _activeTasks.remove(key);\n    _speedTrackers.remove(key);\n    _runningCount--;\n    _processQueue();\n  }\n\n  void _notifyProgress(\n      String recordKey, int episodeNumber, DownloadEpisode episode) {\n    final key = _taskKey(recordKey, episodeNumber);\n    final speed = _speedTrackers[key]?.currentSpeed ?? 0.0;\n    onProgress?.call(recordKey, episodeNumber, episode, speed);\n  }\n\n  Future<String> _fetchM3u8(\n      String url, Map<String, String> headers, CancelToken cancelToken) async {\n    final fetchToken = CancelToken();\n\n    if (cancelToken.isCancelled) {\n      throw DioException(\n        type: DioExceptionType.cancel,\n        requestOptions: RequestOptions(path: url),\n      );\n    }\n\n    try {\n      final response = await _dio.get<String>(\n        url,\n        options: Options(\n          headers: headers,\n          responseType: ResponseType.plain,\n          receiveTimeout: const Duration(seconds: 15),\n        ),\n        cancelToken: fetchToken,\n        onReceiveProgress: (received, total) {\n          if (cancelToken.isCancelled) {\n            fetchToken.cancel('task cancelled');\n            return;\n          }\n          if (received > 2 * 1024 * 1024) {\n            fetchToken.cancel('too large');\n          }\n        },\n      );\n\n      final content = response.data!;\n\n      final trimmed = content.trimLeft();\n      if (!trimmed.startsWith('#EXTM3U')) {\n        throw _NotM3u8Exception('URL 不是 M3U8 播放列表');\n      }\n\n      return content;\n    } on DioException catch (e) {\n      if (cancelToken.isCancelled) rethrow;\n      if (e.type == DioExceptionType.cancel) {\n        throw _NotM3u8Exception('响应过大，非 M3U8 播放列表');\n      }\n      rethrow;\n    }\n  }\n\n  Future<void> _downloadFile(String url, String savePath,\n      Map<String, String> headers, CancelToken cancelToken) async {\n    await _dio.download(\n      url,\n      savePath,\n      options: Options(headers: headers),\n      cancelToken: cancelToken,\n    );\n  }\n\n  Future<int> _downloadSegmentWithRetry(\n    String url,\n    String savePath,\n    Map<String, String> headers,\n    CancelToken cancelToken, {\n    int maxRetries = 3,\n  }) async {\n    final tmpPath = '$savePath.tmp';\n    int retryCount = 0;\n    while (true) {\n      try {\n        await _dio.download(\n          url,\n          tmpPath,\n          options: Options(headers: headers),\n          cancelToken: cancelToken,\n        );\n        await File(tmpPath).rename(savePath);\n        return await File(savePath).length();\n      } catch (e) {\n        try {\n          final tmpFile = File(tmpPath);\n          if (await tmpFile.exists()) await tmpFile.delete();\n        } catch (_) {}\n        if (cancelToken.isCancelled) rethrow;\n        retryCount++;\n        if (retryCount >= maxRetries) rethrow;\n        final delay = Duration(seconds: [1, 3, 9][retryCount - 1]);\n        await Future.delayed(delay);\n      }\n    }\n  }\n\n  @override\n  Future<void> deleteEpisodeFiles(\n      int bangumiId, String pluginName, int episodeNumber) async {\n    final base = await _downloadBaseDir;\n    final dir = Directory(getEpisodeDir(base, bangumiId, pluginName, episodeNumber));\n    if (await dir.exists()) {\n      await dir.delete(recursive: true);\n    }\n  }\n\n  @override\n  Future<void> deleteRecordFiles(int bangumiId, String pluginName) async {\n    final base = await _downloadBaseDir;\n    final dir = Directory('$base/${bangumiId}_$pluginName');\n    if (await dir.exists()) {\n      await dir.delete(recursive: true);\n    }\n  }\n\n  @override\n  String? getLocalVideoPath(DownloadEpisode? episode) {\n    if (episode == null) return null;\n    if (episode.status != DownloadStatus.completed) return null;\n    if (episode.localM3u8Path.isEmpty) return null;\n    final file = File(episode.localM3u8Path);\n    if (!file.existsSync()) return null;\n    return episode.localM3u8Path;\n  }\n}\n\nclass _Semaphore {\n  final int maxCount;\n  int _currentCount = 0;\n  final _waitQueue = <Completer<void>>[];\n\n  _Semaphore(this.maxCount);\n\n  Future<void> acquire() async {\n    if (_currentCount < maxCount) {\n      _currentCount++;\n      return;\n    }\n    final completer = Completer<void>();\n    _waitQueue.add(completer);\n    return completer.future;\n  }\n\n  void release() {\n    if (_waitQueue.isNotEmpty) {\n      final completer = _waitQueue.removeAt(0);\n      completer.complete();\n    } else {\n      _currentCount--;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/extension.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension ImageExtension on num {\n  int cacheSize(BuildContext context) {\n    return (this * MediaQuery.of(context).devicePixelRatio).round();\n  }\n}\n\n"
  },
  {
    "path": "lib/utils/external_player.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass ExternalPlayer {\n  // 注意：仍需开发 iOS/Linux 设备的外部播放功能。\n  // 在 Windows 设备上，对于其他可能的实现，使用 scheme 的方案没有效果。VLC / PotPlayer 等主流播放器更倾向于使用 CLI 命令。\n  // 可行的 iOS 处理代码，请参见 ios/Runner/AppDelegate.swift 的注释部分。\n  static const platform = MethodChannel('com.predidit.kazumi/intent');\n\n  static Future<bool> launchURLWithMIME(String url, String mimeType) async {\n    try {\n      await platform.invokeMethod(\n          'openWithMime', <String, String>{'url': url, 'mimeType': mimeType});\n      return true;\n    } on PlatformException catch (e) {\n      KazumiLogger()\n          .e(\"ExternalPlayer: failed to open with mime\", error: e);\n      return false;\n    }\n  }\n\n  static Future<bool> launchURLWithReferer(String url, String referer) async {\n    try {\n      await platform.invokeMethod(\n          'openWithReferer', <String, String>{'url': url, 'referer': referer});\n      return true;\n    } on PlatformException catch (e) {\n      KazumiLogger()\n          .e(\"ExternalPlayer: failed to open with referer\", error: e);\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/format_utils.dart",
    "content": "String formatBytes(int bytes) {\n  if (bytes < 1024) return '$bytes B';\n  if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';\n  if (bytes < 1024 * 1024 * 1024) {\n    return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';\n  }\n  return '${(bytes / 1024 / 1024 / 1024).toStringAsFixed(1)} GB';\n}\n\nString formatSpeed(double bytesPerSec) {\n  if (bytesPerSec < 1024) return '${bytesPerSec.toStringAsFixed(0)} B/s';\n  if (bytesPerSec < 1024 * 1024) {\n    return '${(bytesPerSec / 1024).toStringAsFixed(1)} KB/s';\n  }\n  return '${(bytesPerSec / 1024 / 1024).toStringAsFixed(1)} MB/s';\n}\n"
  },
  {
    "path": "lib/utils/logger.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:logger/logger.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:synchronized/synchronized.dart';\n\nconst Symbol _forceLogKey = #_forceLog;\n\nclass KazumiLogFilter extends LogFilter {\n  @override\n  bool shouldLog(LogEvent event) {\n    final forceLog = Zone.current[_forceLogKey] as bool? ?? false;\n    if (forceLog) {\n      return true;\n    }\n    return event.level.index >= Logger.level.index;\n  }\n}\n\nclass KazumiLogPrinter extends PrettyPrinter {\n  KazumiLogPrinter()\n      : super(\n          methodCount: 0,\n          errorMethodCount:\n              8,\n          lineLength: 120,\n          colors: true,\n          // Disable emojis for better compatibility\n          printEmojis: false,\n          dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,\n        );\n\n  @override\n  List<String> log(LogEvent event) {\n    // For trace, debug, info - never show stack trace\n    if (event.level == Level.trace ||\n        event.level == Level.debug ||\n        event.level == Level.info) {\n      final messageStr = stringifyMessage(event.message);\n      final time = getTime(event.time);\n      final prefix = _getPrefix(event.level);\n      final levelName = _getLevelName(event.level);\n\n      return [\n        '$prefix $time $levelName $messageStr',\n      ];\n    }\n\n    // For warning, error, fatal - use default behavior which shows stack if provided\n    return super.log(event);\n  }\n\n  /// Colored prefix for log level\n  String _getPrefix(Level level) {\n    if (!colors) return _getLevelTag(level);\n\n    const reset = '\\x1B[0m';\n    String colorCode;\n\n    switch (level) {\n      case Level.trace:\n        colorCode = '\\x1B[90m'; // Bright Black\n      case Level.debug:\n        colorCode = '\\x1B[36m'; // Cyan\n      case Level.info:\n        colorCode = '\\x1B[32m'; // Green\n      case Level.warning:\n        colorCode = '\\x1B[33m'; // Yellow\n      case Level.error:\n        colorCode = '\\x1B[31m'; // Red\n      case Level.fatal:\n        colorCode = '\\x1B[35m'; // Magenta\n      default:\n        colorCode = '';\n    }\n\n    return '$colorCode${_getLevelTag(level)}$reset';\n  }\n\n  /// Tag symbol for log level\n  String _getLevelTag(Level level) {\n    switch (level) {\n      case Level.trace:\n        return '[·]';\n      case Level.debug:\n        return '[*]';\n      case Level.info:\n        return '[i]';\n      case Level.warning:\n        return '[!]';\n      case Level.error:\n        return '[×]';\n      case Level.fatal:\n        return '[‼]';\n      default:\n        return '[-]';\n    }\n  }\n\n  String _getLevelName(Level level) {\n    return level.name.toUpperCase().padRight(7);\n  }\n}\n\nclass KazumiLogOutput extends LogOutput {\n  static final Lock _logLock = Lock();\n  static String? _logFilePath;\n\n  static Future<String> _getLogFilePath() async {\n    if (_logFilePath != null) return _logFilePath!;\n\n    final dir = (await getApplicationSupportDirectory()).path;\n    final logDir = p.join(dir, \"logs\");\n    final directory = Directory(logDir);\n    if (!await directory.exists()) {\n      await directory.create(recursive: true);\n    }\n    _logFilePath = p.join(logDir, \"kazumi_logs.log\");\n    return _logFilePath!;\n  }\n\n  @override\n  void output(OutputEvent event) {\n    for (var line in event.lines) {\n      print(line);\n    }\n\n    // Write to file if: warning/error/fatal OR forceLog is enabled\n    final forceLog = Zone.current[_forceLogKey] as bool? ?? false;\n    if (event.level.index >= Level.warning.index || forceLog) {\n      _writeToFile(event);\n    }\n  }\n\n  void _writeToFile(OutputEvent event) {\n    _logLock.synchronized(() async {\n      try {\n        final filePath = await _getLogFilePath();\n        final file = File(filePath);\n\n        final timestamp = DateTime.now().toString();\n\n        final buffer = StringBuffer();\n        buffer.writeln('[$timestamp]');\n        for (var line in event.lines) {\n          final cleanLine = _removeAnsiCodes(line);\n          buffer.writeln(cleanLine);\n        }\n        buffer.writeln();\n\n        await file.writeAsString(\n          buffer.toString(),\n          mode: FileMode.writeOnlyAppend,\n        );\n      } catch (e) {\n        print('Failed to write log to file: $e');\n      }\n    });\n  }\n\n  /// Remove ANSI escape codes from string to ensure clean log files\n  String _removeAnsiCodes(String text) {\n    return text.replaceAll(RegExp(r'\\x1B\\[[0-9;]*m'), '');\n  }\n}\n\nclass KazumiLogger {\n  KazumiLogger._internal() {\n    _logger = Logger(\n      filter: KazumiLogFilter(),\n      printer: KazumiLogPrinter(),\n      output: KazumiLogOutput(),\n    );\n  }\n\n  static final KazumiLogger _instance = KazumiLogger._internal();\n  factory KazumiLogger() {\n    return _instance;\n  }\n\n  late final Logger _logger;\n  void _log(void Function() logFn, bool forceLog) {\n    if (forceLog) {\n      runZoned(logFn, zoneValues: {_forceLogKey: true});\n    } else {\n      logFn();\n    }\n  }\n\n  /// Trace log - lowest level, very detailed information\n  void t(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.t(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n\n  /// Debug log - detailed information for debugging\n  void d(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.d(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n\n  /// Info log - informational messages\n  void i(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.i(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n\n  /// Warning log - potentially harmful situations\n  void w(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.w(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n\n  /// Error log - error events that might still allow the app to continue\n  void e(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.e(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n\n  /// Fatal log - very severe error events that will presumably lead the app to abort\n  void f(dynamic message,\n      {Object? error, StackTrace? stackTrace, bool forceLog = false}) {\n    _log(() => _logger.f(message, error: error, stackTrace: stackTrace), forceLog);\n  }\n}\n\nFuture<File> getLogsPath() async {\n  final dir = (await getApplicationSupportDirectory()).path;\n  final logDir = p.join(dir, \"logs\");\n  final filename = p.join(logDir, \"kazumi_logs.log\");\n\n  final directory = Directory(logDir);\n  if (!await directory.exists()) {\n    await directory.create(recursive: true);\n  }\n\n  final file = File(filename);\n  if (!await file.exists()) {\n    await KazumiLogOutput._logLock.synchronized(() async {\n      if (!await file.exists()) {\n        await file.create();\n      }\n    });\n  }\n  return file;\n}\n\nFuture<bool> clearLogs() async {\n  try {\n    final file = await getLogsPath();\n    await KazumiLogOutput._logLock.synchronized(() async {\n      await file.writeAsString('');\n    });\n    return true;\n  } catch (e) {\n    print('Error clearing file: $e');\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/utils/m3u8_ad_filter.dart",
    "content": "import 'package:kazumi/utils/m3u8_parser.dart';\n\nclass M3u8AdFilter {\n  /// Filter ad segments from a media playlist.\n  /// Mimics FFmpeg hls_ad_filter behavior using discontinuity groups.\n  static List<M3u8Segment> filterAds(List<M3u8Segment> segments) {\n    if (segments.isEmpty) return segments;\n\n    // Group segments by discontinuityGroup\n    final groups = <int, List<M3u8Segment>>{};\n    for (final seg in segments) {\n      groups.putIfAbsent(seg.discontinuityGroup, () => []);\n      groups[seg.discontinuityGroup]!.add(seg);\n    }\n\n    // Only one group means no ads detected\n    if (groups.length <= 1) return segments;\n\n    // Calculate total duration per group\n    final groupDurations = <int, double>{};\n    for (final entry in groups.entries) {\n      groupDurations[entry.key] = entry.value.fold<double>(\n        0.0,\n        (sum, seg) => sum + seg.duration,\n      );\n    }\n\n    // Find the longest group as the \"main content\" reference\n    double maxDuration = 0;\n    for (final d in groupDurations.values) {\n      if (d > maxDuration) maxDuration = d;\n    }\n\n    // Identify ad groups\n    final adGroups = <int>{};\n    final sortedKeys = groups.keys.toList()..sort();\n\n    for (final groupId in sortedKeys) {\n      final groupDuration = groupDurations[groupId]!;\n\n      // Skip the main content group\n      if (groupDuration == maxDuration) continue;\n\n      bool isAd = false;\n\n      // Short segments relative to main content (< 30%)\n      if (groupDuration < maxDuration * 0.3) {\n        isAd = true;\n      }\n\n      // First or last group with short duration (< 30s)\n      if ((groupId == sortedKeys.first || groupId == sortedKeys.last) &&\n          groupDuration < 30.0) {\n        isAd = true;\n      }\n\n      // Very short segments (< 10s) are almost certainly ads\n      if (groupDuration < 10.0) {\n        isAd = true;\n      }\n\n      if (isAd) {\n        adGroups.add(groupId);\n      }\n    }\n\n    if (adGroups.isEmpty) return segments;\n\n    // Remove ad segments\n    return segments\n        .where((seg) => !adGroups.contains(seg.discontinuityGroup))\n        .toList();\n  }\n\n  /// Calculate the new target duration after filtering\n  static double calculateTargetDuration(List<M3u8Segment> segments) {\n    if (segments.isEmpty) return 0;\n    double maxSegDuration = 0;\n    for (final seg in segments) {\n      if (seg.duration > maxSegDuration) {\n        maxSegDuration = seg.duration;\n      }\n    }\n    return maxSegDuration;\n  }\n}\n"
  },
  {
    "path": "lib/utils/m3u8_parser.dart",
    "content": "class M3u8Key {\n  final String method;\n  final String uri;\n  final String? iv;\n\n  M3u8Key({required this.method, required this.uri, this.iv});\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is M3u8Key &&\n          method == other.method &&\n          uri == other.uri &&\n          iv == other.iv;\n\n  @override\n  int get hashCode => Object.hash(method, uri, iv);\n\n  @override\n  String toString() {\n    final sb = StringBuffer('#EXT-X-KEY:METHOD=$method,URI=\"$uri\"');\n    if (iv != null) {\n      sb.write(',IV=$iv');\n    }\n    return sb.toString();\n  }\n}\n\nclass M3u8Segment {\n  final double duration;\n  final String uri;\n  final int discontinuityGroup;\n  final M3u8Key? key;\n\n  M3u8Segment({\n    required this.duration,\n    required this.uri,\n    required this.discontinuityGroup,\n    this.key,\n  });\n}\n\nclass M3u8Variant {\n  final int bandwidth;\n  final String? resolution;\n  final String uri;\n\n  M3u8Variant({required this.bandwidth, this.resolution, required this.uri});\n}\n\nclass M3u8MasterPlaylist {\n  final List<M3u8Variant> variants;\n\n  M3u8MasterPlaylist({required this.variants});\n\n  M3u8Variant get bestVariant {\n    return variants.reduce((a, b) => a.bandwidth > b.bandwidth ? a : b);\n  }\n}\n\nclass M3u8MediaPlaylist {\n  final List<M3u8Segment> segments;\n  final double targetDuration;\n  final bool isVod;\n\n  M3u8MediaPlaylist({\n    required this.segments,\n    required this.targetDuration,\n    required this.isVod,\n  });\n}\n\nenum M3u8Type { master, media }\n\nclass M3u8Parser {\n  static M3u8Type detectType(String content) {\n    if (content.contains('#EXT-X-STREAM-INF')) {\n      return M3u8Type.master;\n    }\n    return M3u8Type.media;\n  }\n\n  static String resolveUrl(String baseUrl, String relativeUrl) {\n    if (relativeUrl.startsWith('http://') || relativeUrl.startsWith('https://')) {\n      return relativeUrl;\n    }\n    final baseUri = Uri.parse(baseUrl);\n    if (relativeUrl.startsWith('/')) {\n      return '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}$relativeUrl';\n    }\n    final basePath = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);\n    return '$basePath$relativeUrl';\n  }\n\n  static M3u8MasterPlaylist parseMasterPlaylist(String content, String baseUrl) {\n    final lines = content.split('\\n').map((l) => l.trim()).toList();\n    final variants = <M3u8Variant>[];\n\n    for (int i = 0; i < lines.length; i++) {\n      final line = lines[i];\n      if (line.startsWith('#EXT-X-STREAM-INF:')) {\n        final attrs = line.substring('#EXT-X-STREAM-INF:'.length);\n        int bandwidth = 0;\n        String? resolution;\n\n        final bandwidthMatch = RegExp(r'BANDWIDTH=(\\d+)').firstMatch(attrs);\n        if (bandwidthMatch != null) {\n          bandwidth = int.parse(bandwidthMatch.group(1)!);\n        }\n\n        final resolutionMatch = RegExp(r'RESOLUTION=([^\\s,]+)').firstMatch(attrs);\n        if (resolutionMatch != null) {\n          resolution = resolutionMatch.group(1);\n        }\n\n        if (i + 1 < lines.length && !lines[i + 1].startsWith('#')) {\n          final uri = resolveUrl(baseUrl, lines[i + 1]);\n          variants.add(M3u8Variant(bandwidth: bandwidth, resolution: resolution, uri: uri));\n        }\n      }\n    }\n\n    return M3u8MasterPlaylist(variants: variants);\n  }\n\n  static M3u8MediaPlaylist parseMediaPlaylist(String content, String baseUrl) {\n    final lines = content.split('\\n').map((l) => l.trim()).toList();\n    final segments = <M3u8Segment>[];\n    double targetDuration = 0;\n    bool hasEndList = false;\n    bool isExplicitVod = false;\n    bool isLiveEvent = false;\n    int currentDiscontinuityGroup = 0;\n    M3u8Key? currentKey;\n    double currentDuration = 0;\n\n    for (int i = 0; i < lines.length; i++) {\n      final line = lines[i];\n\n      if (line.startsWith('#EXT-X-TARGETDURATION:')) {\n        targetDuration = double.parse(line.substring('#EXT-X-TARGETDURATION:'.length));\n      } else if (line == '#EXT-X-ENDLIST') {\n        hasEndList = true;\n      } else if (line == '#EXT-X-PLAYLIST-TYPE:VOD') {\n        isExplicitVod = true;\n      } else if (line == '#EXT-X-PLAYLIST-TYPE:EVENT') {\n        isLiveEvent = true;\n      } else if (line == '#EXT-X-DISCONTINUITY') {\n        currentDiscontinuityGroup++;\n      } else if (line.startsWith('#EXT-X-KEY:')) {\n        currentKey = _parseKey(line, baseUrl);\n      } else if (line.startsWith('#EXTINF:')) {\n        final durationStr = line.substring('#EXTINF:'.length).split(',')[0];\n        currentDuration = double.parse(durationStr);\n      } else if (line.isNotEmpty && !line.startsWith('#')) {\n        final uri = resolveUrl(baseUrl, line);\n        segments.add(M3u8Segment(\n          duration: currentDuration,\n          uri: uri,\n          discontinuityGroup: currentDiscontinuityGroup,\n          key: currentKey,\n        ));\n        currentDuration = 0;\n      }\n    }\n\n    // Consider it VOD if:\n    // 1. Has #EXT-X-ENDLIST, or\n    // 2. Has #EXT-X-PLAYLIST-TYPE:VOD, or\n    // 3. Has finite segments and is not explicitly a live EVENT stream.\n    // Many third-party video sources omit #EXT-X-ENDLIST for VOD content.\n    final bool isVod = hasEndList || isExplicitVod || (!isLiveEvent && segments.isNotEmpty);\n\n    return M3u8MediaPlaylist(\n      segments: segments,\n      targetDuration: targetDuration,\n      isVod: isVod,\n    );\n  }\n\n  static M3u8Key? _parseKey(String line, String baseUrl) {\n    final attrs = line.substring('#EXT-X-KEY:'.length);\n\n    final methodMatch = RegExp(r'METHOD=([^,]+)').firstMatch(attrs);\n    final method = methodMatch?.group(1) ?? 'NONE';\n\n    if (method == 'NONE') return null;\n\n    final uriMatch = RegExp(r'URI=\"([^\"]+)\"').firstMatch(attrs);\n    final uri = uriMatch != null ? resolveUrl(baseUrl, uriMatch.group(1)!) : '';\n\n    final ivMatch = RegExp(r'IV=(0x[0-9a-fA-F]+)').firstMatch(attrs);\n    final iv = ivMatch?.group(1);\n\n    return M3u8Key(method: method, uri: uri, iv: iv);\n  }\n\n  static List<M3u8Key> extractUniqueKeys(M3u8MediaPlaylist playlist) {\n    final seen = <String>{};\n    final keys = <M3u8Key>[];\n    for (final seg in playlist.segments) {\n      if (seg.key != null && !seen.contains(seg.key!.uri)) {\n        seen.add(seg.key!.uri);\n        keys.add(seg.key!);\n      }\n    }\n    return keys;\n  }\n\n  static String buildLocalM3u8(\n    List<M3u8Segment> segments, {\n    required double targetDuration,\n    Map<String, String> keyUriToLocal = const {},\n  }) {\n    final sb = StringBuffer();\n    sb.writeln('#EXTM3U');\n    sb.writeln('#EXT-X-VERSION:3');\n    sb.writeln('#EXT-X-TARGETDURATION:${targetDuration.ceil()}');\n    sb.writeln('#EXT-X-MEDIA-SEQUENCE:0');\n\n    int lastDiscontinuityGroup = 0;\n    M3u8Key? lastKey;\n\n    for (int i = 0; i < segments.length; i++) {\n      final seg = segments[i];\n\n      if (seg.discontinuityGroup != lastDiscontinuityGroup && i > 0) {\n        sb.writeln('#EXT-X-DISCONTINUITY');\n        lastDiscontinuityGroup = seg.discontinuityGroup;\n      }\n\n      if (seg.key != lastKey) {\n        if (seg.key == null) {\n          sb.writeln('#EXT-X-KEY:METHOD=NONE');\n        } else {\n          final localUri = keyUriToLocal[seg.key!.uri] ?? seg.key!.uri;\n          final keySb = StringBuffer('#EXT-X-KEY:METHOD=${seg.key!.method},URI=\"$localUri\"');\n          if (seg.key!.iv != null) {\n            keySb.write(',IV=${seg.key!.iv}');\n          }\n          sb.writeln(keySb.toString());\n        }\n        lastKey = seg.key;\n      }\n\n      sb.writeln('#EXTINF:${seg.duration.toStringAsFixed(6)},');\n      sb.writeln('seg_${i.toString().padLeft(5, '0')}.ts');\n    }\n\n    sb.writeln('#EXT-X-ENDLIST');\n    return sb.toString();\n  }\n\n  static bool _isM3u8Url(String url) {\n    final path = Uri.parse(url).path.toLowerCase();\n    return path.endsWith('.m3u8');\n  }\n\n  /// 展开嵌套 M3U8 片段。\n  /// [fetcher] 异步回调，给定 URL 返回 M3U8 文本内容。\n  /// [maxDepth] 递归深度上限，防止无限嵌套。\n  static Future<List<M3u8Segment>> resolveNestedSegments(\n    List<M3u8Segment> segments,\n    Future<String> Function(String url) fetcher, {\n    int maxDepth = 3,\n  }) async {\n    if (maxDepth <= 0) return segments;\n    if (!segments.any((s) => _isM3u8Url(s.uri))) return segments;\n\n    final result = <M3u8Segment>[];\n    int groupOffset = 0;\n\n    for (final seg in segments) {\n      if (!_isM3u8Url(seg.uri)) {\n        result.add(M3u8Segment(\n          duration: seg.duration,\n          uri: seg.uri,\n          discontinuityGroup: seg.discontinuityGroup + groupOffset,\n          key: seg.key,\n        ));\n        continue;\n      }\n\n      // 该 segment 的 URI 指向嵌套 m3u8，展开\n      try {\n        final content = await fetcher(seg.uri);\n        final nested = parseMediaPlaylist(content, seg.uri);\n        final resolved = await resolveNestedSegments(\n          nested.segments, fetcher, maxDepth: maxDepth - 1,\n        );\n\n        if (resolved.isEmpty) continue;\n\n        final nestedBase = seg.discontinuityGroup + groupOffset;\n        int maxNestedGroup = 0;\n        for (final ns in resolved) {\n          if (ns.discontinuityGroup > maxNestedGroup) {\n            maxNestedGroup = ns.discontinuityGroup;\n          }\n        }\n\n        for (final ns in resolved) {\n          result.add(M3u8Segment(\n            duration: ns.duration,\n            uri: ns.uri,\n            discontinuityGroup: ns.discontinuityGroup + nestedBase,\n            key: ns.key,\n          ));\n        }\n\n        // 后续 segment 的 group 需要额外偏移，避免碰撞\n        groupOffset += maxNestedGroup;\n      } catch (e) {\n        // 获取/解析失败，保留原始 segment\n        result.add(M3u8Segment(\n          duration: seg.duration,\n          uri: seg.uri,\n          discontinuityGroup: seg.discontinuityGroup + groupOffset,\n          key: seg.key,\n        ));\n      }\n    }\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/utils/mortis.dart",
    "content": "// The file contains some interesting imformation about dandan public api.\n// Unusual name to avoid github global search, it's just test code and will be replaced in github actions.\nconst Map<String, String> mortis = {\n  'id': 'kvpx7qkqjh',\n  'value': 'rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax',\n};"
  },
  {
    "path": "lib/utils/proxy_manager.dart",
    "content": "import 'package:kazumi/request/request.dart';\n\n/// 代理管理器\n/// 统一管理 Dio HTTP 请求的代理设置\n/// 注意：WebView 代理在各平台 controller 初始化时单独处理\nclass ProxyManager {\n  ProxyManager._();\n\n  /// 应用代理设置\n  static void applyProxy() {\n    Request.setProxy();\n  }\n\n  /// 清除代理设置\n  static void clearProxy() {\n    Request.disableProxy();\n  }\n}\n"
  },
  {
    "path": "lib/utils/proxy_utils.dart",
    "content": "/// 代理相关的工具函数\nclass ProxyUtils {\n  // 防止实例化\n  ProxyUtils._();\n\n  /// 解析代理 URL，返回 (主机, 端口)\n  ///\n  /// 支持的格式:\n  /// - http://127.0.0.1:7890\n  /// - 127.0.0.1:7890\n  static (String, int)? parseProxyUrl(String url) {\n    url = url.trim();\n    if (url.isEmpty) return null;\n\n    String hostPort = url;\n\n    // 移除 http:// 前缀\n    if (url.toLowerCase().startsWith('http://')) {\n      hostPort = url.substring(7);\n    } else if (url.toLowerCase().startsWith('https://')) {\n      hostPort = url.substring(8);\n    }\n\n    // 解析主机和端口\n    final parts = hostPort.split(':');\n    if (parts.length != 2) return null;\n\n    final host = parts[0];\n    final port = int.tryParse(parts[1]);\n    if (host.isEmpty || port == null) return null;\n\n    return (host, port);\n  }\n\n  /// 获取格式化的代理 URL（用于 mpv）\n  static String? getFormattedProxyUrl(String url) {\n    final parsed = parseProxyUrl(url);\n    if (parsed == null) return null;\n    return 'http://${parsed.$1}:${parsed.$2}';\n  }\n\n  /// 验证代理 URL 是否有效\n  static bool isValidProxyUrl(String url) {\n    return parseProxyUrl(url) != null;\n  }\n}\n"
  },
  {
    "path": "lib/utils/remote.dart",
    "content": "import 'dart:async';\n\nimport 'package:dlna_dart/dlna.dart';\nimport 'package:flutter/material.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass RemotePlay {\n  Future<void> castVideo(String video, String referer) async {\n    final searcher = DLNAManager();\n    final dlna = await searcher.start();\n    List<Widget> dlnaDevice = [];\n    await KazumiDialog.show(builder: (BuildContext context) {\n      return StatefulBuilder(builder: (context, setState) {\n        return AlertDialog(\n          title: const Text('远程投屏'),\n          content: SingleChildScrollView(\n            child: Column(\n              children: dlnaDevice,\n            ),\n          ),\n          actions: [\n            const SizedBox(width: 20),\n            TextButton(\n              onPressed: () {\n                KazumiDialog.dismiss();\n              },\n              child: Text(\n                '退出',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n            TextButton(\n                onPressed: () {\n                  setState(() {});\n                  KazumiDialog.showToast(\n                    message: '开始搜索',\n                  );\n                  dlna.devices.stream.listen((deviceList) {\n                    dlnaDevice = [];\n                    deviceList.forEach((key, value) async {\n                      KazumiLogger().i('RemotePlay: key: $key');\n                      KazumiLogger().i(\n                          'RemotePlay: value: ${value.info.friendlyName} ${value.info.deviceType} ${value.info.URLBase}');\n                      setState(() {\n                        dlnaDevice.add(ListTile(\n                            leading: _deviceUPnPIcon(\n                                value.info.deviceType.split(':')[3]),\n                            title: Text(value.info.friendlyName),\n                            subtitle: Text(value.info.deviceType.split(':')[3]),\n                            onTap: () {\n                              try {\n                                KazumiDialog.showToast(\n                                  message: '尝试投屏至 ${value.info.friendlyName}',\n                                );\n                                DLNADevice(value.info).setUrl(video);\n                                DLNADevice(value.info).play();\n                              } catch (e) {\n                                KazumiLogger()\n                                    .e('RemotePlay: failed to cast to device', error: e);\n                                KazumiDialog.showToast(\n                                  message: 'DLNA 异常: $e \\n尝试重新进入 DLNA 投屏或切换设备',\n                                );\n                              }\n                            }));\n                      });\n                    });\n                  });\n                  // Timer(const Duration(seconds: 30), () {\n                  //   KazumiDialog.showToast(\n                  //     message: '已搜索30s，若未发现设备请尝试重新进入 DLNA 投屏',\n                  //   );\n                  // });\n                },\n                child: Text(\n                  '搜索',\n                  style:\n                      TextStyle(color: Theme.of(context).colorScheme.outline),\n                )),\n          ],\n        );\n      });\n    }, onDismiss: () {\n      searcher.stop();\n    });\n  }\n\n  Icon _deviceUPnPIcon(String deviceType) {\n    switch (deviceType) {\n      case 'MediaRenderer':\n        return const Icon(Icons.cast_connected);\n      case 'MediaServer':\n        return const Icon(Icons.cast_connected);\n      case 'InternetGatewayDevice':\n        return const Icon(Icons.router);\n      case 'BasicDevice':\n        return const Icon(Icons.device_hub);\n      case 'DimmableLight':\n        return const Icon(Icons.lightbulb);\n      case 'WLANAccessPoint':\n        return const Icon(Icons.lan);\n      case 'WLANConnectionDevice':\n        return const Icon(Icons.wifi_tethering);\n      case 'Printer':\n        return const Icon(Icons.print);\n      case 'Scanner':\n        return const Icon(Icons.scanner);\n      case 'DigitalSecurityCamera':\n        return const Icon(Icons.camera_enhance_outlined);\n      default:\n        return const Icon(Icons.question_mark);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/search_parser.dart",
    "content": "class SearchParser {\n  final String query;\n  final RegExp _idRegExp = RegExp(r'id:(\\d+)', caseSensitive: false);\n  final RegExp _tagRegExp = RegExp(r'tag:([\\w\\u4e00-\\u9fa5\\u30A0-\\u30FF\\.\\-]+)', caseSensitive: false);\n  final RegExp _sortRegExp = RegExp(r'sort:([\\w\\-]+)', caseSensitive: false);\n\n  SearchParser(this.query);\n\n  String? parseId() {\n    final match = _idRegExp.firstMatch(query);\n    return match != null ? match.group(1) : null;\n  }\n\n  String? parseTag() {\n    final match = _tagRegExp.firstMatch(query);\n    return match != null ? match.group(1) : null;\n  }\n\n  String? parseSort() {\n    final match = _sortRegExp.firstMatch(query);\n    return match != null ? match.group(1) : null;\n  }\n\n  String parseKeywords() {\n    String cleaned = query.replaceAll(_idRegExp, '');\n    cleaned = cleaned.replaceAll(_tagRegExp, '');\n    cleaned = cleaned.replaceAll(_sortRegExp, '');\n    return cleaned.trim();\n  }\n\n  bool hasSortSyntax() {\n    return _sortRegExp.hasMatch(query);\n  }\n\n  String removeSort() {\n    return query.replaceAll(_sortRegExp, '').trim();\n  }\n\n  String updateSort(String sortValue) {\n    if (hasSortSyntax()) {\n      return query.replaceAllMapped(_sortRegExp, (match) => 'sort:$sortValue');\n    } else {\n      return '${query.trim()} sort:$sortValue'.trim();\n    }\n  }\n}"
  },
  {
    "path": "lib/utils/storage.dart",
    "content": "import 'dart:io';\nimport 'package:hive_ce/hive.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_item.dart';\nimport 'package:kazumi/modules/bangumi/bangumi_tag.dart';\nimport 'package:kazumi/modules/history/history_module.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:kazumi/modules/collect/collect_change_module.dart';\nimport 'package:kazumi/modules/search/search_history_module.dart';\nimport 'package:kazumi/modules/download/download_module.dart';\n\nclass GStorage {\n  /// Don't use favorites box, it's replaced by collectibles.\n  static late Box<BangumiItem> favorites;\n  static late Box<CollectedBangumi> collectibles;\n  static late Box<History> histories;\n  static late Box<CollectedBangumiChange> collectChanges;\n  static late Box<String> shieldList;\n  static late final Box<dynamic> setting;\n  static late Box<SearchHistory> searchHistory;\n  static late Box<DownloadRecord> downloads;\n\n  /// Hive directory path, initialized during init()\n  static String? _hivePath;\n\n  static Future init() async {\n    _hivePath = '${(await getApplicationSupportDirectory()).path}/hive';\n\n    Hive.registerAdapter(BangumiItemAdapter());\n    Hive.registerAdapter(BangumiTagAdapter());\n    Hive.registerAdapter(CollectedBangumiAdapter());\n    Hive.registerAdapter(ProgressAdapter());\n    Hive.registerAdapter(HistoryAdapter());\n    Hive.registerAdapter(CollectedBangumiChangeAdapter());\n    Hive.registerAdapter(SearchHistoryAdapter());\n    Hive.registerAdapter(DownloadRecordAdapter());\n    Hive.registerAdapter(DownloadEpisodeAdapter());\n\n    // Open each box with automatic recovery on corruption\n    favorites = await _openBoxSafe<BangumiItem>('favorites');\n    collectibles = await _openBoxSafe<CollectedBangumi>('collectibles');\n    histories = await _openBoxSafe<History>('histories');\n    setting = await _openBoxSafe<dynamic>('setting');\n    collectChanges = await _openBoxSafe<CollectedBangumiChange>('collectchanges');\n    shieldList = await _openBoxSafe<String>('shieldList');\n    searchHistory = await _openBoxSafe<SearchHistory>('searchHistory');\n    downloads = await _openBoxSafe<DownloadRecord>('downloads');\n  }\n\n  /// Open a Hive box with automatic recovery on corruption.\n  /// If the box is corrupted, delete it and create a new empty one.\n  static Future<Box<T>> _openBoxSafe<T>(String boxName) async {\n    try {\n      return await Hive.openBox<T>(boxName);\n    } catch (e) {\n      KazumiLogger().e('GStorage: Box \"$boxName\" corrupted, attempting recovery', error: e);\n\n      // Delete the corrupted box files\n      await _deleteBoxFiles(boxName);\n\n      // Try to open again (will create a new empty box)\n      try {\n        final box = await Hive.openBox<T>(boxName);\n        KazumiLogger().i('GStorage: Box \"$boxName\" recovered successfully (data lost)');\n        return box;\n      } catch (e2) {\n        KazumiLogger().e('GStorage: Failed to recover box \"$boxName\"', error: e2);\n        rethrow;\n      }\n    }\n  }\n\n  /// Delete Hive box files for a given box name\n  static Future<void> _deleteBoxFiles(String boxName) async {\n    if (_hivePath == null) return;\n\n    final boxFile = File('$_hivePath/$boxName.hive');\n    final lockFile = File('$_hivePath/$boxName.lock');\n\n    try {\n      if (await boxFile.exists()) {\n        await boxFile.delete();\n        KazumiLogger().i('GStorage: Deleted corrupted box file: $boxName.hive');\n      }\n      if (await lockFile.exists()) {\n        await lockFile.delete();\n        KazumiLogger().i('GStorage: Deleted lock file: $boxName.lock');\n      }\n    } catch (e) {\n      KazumiLogger().e('GStorage: Failed to delete box files for \"$boxName\"', error: e);\n    }\n  }\n\n  static Future<void> backupBox(String boxName, String backupFilePath) async {\n    final appDocumentDir = await getApplicationSupportDirectory();\n    final hiveBoxFile = File('${appDocumentDir.path}/hive/$boxName.hive');\n    if (await hiveBoxFile.exists()) {\n      await hiveBoxFile.copy(backupFilePath);\n      print('Backup success: $backupFilePath');\n    } else {\n      print('Hive box not exists');\n    }\n  }\n\n  static Future<void> patchHistory(String backupFilePath) async {\n    final backupFile = File(backupFilePath);\n    final backupContent = await backupFile.readAsBytes();\n    final tempBox = await Hive.openBox('tempHistoryBox', bytes: backupContent);\n    final tempBoxItems = tempBox.toMap().entries;\n\n    for (var tempBoxItem in tempBoxItems) {\n      if (histories.get(tempBoxItem.key) != null) {\n        if (histories\n            .get(tempBoxItem.key)!\n            .lastWatchTime\n            .isBefore(tempBoxItem.value.lastWatchTime)) {\n          await histories.delete(tempBoxItem.key);\n          await histories.put(tempBoxItem.key, tempBoxItem.value);\n        }\n      } else {\n        await histories.put(tempBoxItem.key, tempBoxItem.value);\n      }\n    }\n    await tempBox.close();\n  }\n\n  static Future<void> restoreCollectibles(String backupFilePath) async {\n    final backupFile = File(backupFilePath);\n    final backupContent = await backupFile.readAsBytes();\n    final tempBox =\n        await Hive.openBox('tempCollectiblesBox', bytes: backupContent);\n    final tempBoxItems = tempBox.toMap().entries;\n    KazumiLogger().i(\n        'WebDav: restoring collectibles. tempCollectiblesBox length ${tempBoxItems.length}');\n\n    await collectibles.clear();\n    for (var tempBoxItem in tempBoxItems) {\n      await collectibles.put(tempBoxItem.key, tempBoxItem.value);\n    }\n    await tempBox.close();\n  }\n\n  static Future<List<CollectedBangumi>> getCollectiblesFromFile(\n      String backupFilePath) async {\n    final backupFile = File(backupFilePath);\n    final backupContent = await backupFile.readAsBytes();\n    final tempBox =\n        await Hive.openBox('tempCollectiblesBox', bytes: backupContent);\n    final tempBoxItems = tempBox.toMap().entries;\n    KazumiLogger().i(\n        'WebDav: get collectibles from file. tempCollectiblesBox length ${tempBoxItems.length}');\n\n    final List<CollectedBangumi> collectibles = [];\n    for (var tempBoxItem in tempBoxItems) {\n      collectibles.add(tempBoxItem.value);\n    }\n    await tempBox.close();\n    return collectibles;\n  }\n\n  static Future<List<CollectedBangumiChange>> getCollectChangesFromFile(\n      String backupFilePath) async {\n    final backupFile = File(backupFilePath);\n    final backupContent = await backupFile.readAsBytes();\n    final tempBox =\n        await Hive.openBox('tempCollectChangesBox', bytes: backupContent);\n    final tempBoxItems = tempBox.toMap().entries;\n    KazumiLogger().i(\n        'WebDav: get collectChanges from file. tempCollectChangesBox length ${tempBoxItems.length}');\n\n    final List<CollectedBangumiChange> collectChanges = [];\n    for (var tempBoxItem in tempBoxItems) {\n      collectChanges.add(tempBoxItem.value);\n    }\n    await tempBox.close();\n    return collectChanges;\n  }\n\n  static Future<void> patchCollectibles(\n      List<CollectedBangumi> remoteCollectibles,\n      List<CollectedBangumiChange> remoteChanges) async {\n    List<CollectedBangumi> localCollectibles = collectibles.values.toList();\n    List<CollectedBangumiChange> localChanges = collectChanges.values.toList();\n\n    final List<CollectedBangumiChange> newLocalChanges =\n        localChanges.where((localChange) {\n      return !remoteChanges\n          .any((remoteChange) => remoteChange.id == localChange.id);\n    }).toList();\n\n    newLocalChanges.sort((a, b) => a.timestamp.compareTo(b.timestamp));\n\n    // Process local changes\n    for (var change in newLocalChanges) {\n      // For delete action, we don't need to look up the local collectible.\n      // We can directly remove the item from the remote list.\n      if (change.action == 3) {\n        // Action 3: delete\n        remoteCollectibles\n            .removeWhere((b) => b.bangumiItem.id == change.bangumiID);\n      } else {\n        // For add/update, we still need to look up the local collectible.\n        final changedBangumiID = change.bangumiID.toString();\n        for (var localCollect in localCollectibles) {\n          if (localCollect.bangumiItem.id.toString() == changedBangumiID) {\n            if (change.action == 1) {\n              // Action 1: add\n              final exists = remoteCollectibles\n                  .any((b) => b.bangumiItem.id == localCollect.bangumiItem.id);\n              if (!exists) {\n                remoteCollectibles.add(localCollect);\n              } else {\n                final index = remoteCollectibles.indexWhere(\n                    (b) => b.bangumiItem.id == localCollect.bangumiItem.id);\n                localCollect.type = change.type;\n                if (index != -1) {\n                  // Update the entry with local data.\n                  remoteCollectibles[index] = localCollect;\n                }\n              }\n            } else if (change.action == 2) {\n              // Action 2: update\n              final index = remoteCollectibles.indexWhere(\n                  (b) => b.bangumiItem.id == localCollect.bangumiItem.id);\n              localCollect.type = change.type;\n              if (index != -1) {\n                // Update the entry with local data.\n                remoteCollectibles[index] = localCollect;\n              }\n            }\n            break;\n          }\n        }\n      }\n    }\n\n    // merge local changes with remote changes\n    final Map<int, CollectedBangumiChange> mergedMap = {};\n    for (var change in remoteChanges) {\n      mergedMap[change.id] = change;\n    }\n    for (var change in newLocalChanges) {\n      if (!mergedMap.containsKey(change.id)) {\n        mergedMap[change.id] = change;\n      }\n    }\n    final List<CollectedBangumiChange> mergedChanges =\n        mergedMap.values.toList();\n\n    // Update local storage\n    await collectibles.clear();\n    for (var collect in remoteCollectibles) {\n      await collectibles.put(collect.bangumiItem.id, collect);\n    }\n    await collectChanges.clear();\n    for (var change in mergedChanges) {\n      await collectChanges.put(change.id, change);\n    }\n  }\n\n  // Prevent instantiation\n  GStorage._();\n}\n\nclass SettingBoxKey {\n  static const String hAenable = 'hAenable',\n      hardwareDecoder = 'hardwareDecoder',\n      searchEnhanceEnable = 'searchEnhanceEnable',\n      autoUpdate = 'autoUpdate',\n      alwaysOntop = 'alwaysOntop',\n      defaultPlaySpeed = 'defaultPlaySpeed',\n      defaultShortcutForwardPlaySpeed = 'defaultShortcutForwardPlaySpeed',\n      defaultAspectRatioType = 'defaultAspectRatioType',\n      buttonSkipTime = 'buttonSkipTime',\n      arrowKeySkipTime = 'arrowKeySkipTime',\n      danmakuEnhance = 'danmakuEnhance',\n      danmakuBorder = 'danmakuBorder',\n      danmakuBorderSize = 'danmakuBorderSize',\n      danmakuOpacity = 'danmakuOpacity',\n      danmakuFontSize = 'danmakuFontSize',\n      danmakuTop = 'danmakuTop',\n      danmakuScroll = 'danmakuScroll',\n      danmakuBottom = 'danmakuBottom',\n      danmakuMassive = 'danmakuMassive',\n      danmakuDeduplication = 'danmakuDeduplication',\n      danmakuArea = 'danmakuArea',\n      danmakuColor = 'danmakuColor',\n      danmakuDuration = 'danmakuDuration',\n      danmakuLineHeight = 'danmakuLineHeight',\n      danmakuEnabledByDefault = 'danmakuEnabledByDefault',\n      danmakuBiliBiliSource = 'danmakuBiliBiliSource',\n      danmakuGamerSource = 'danmakuGamerSource',\n      danmakuDanDanSource = 'danmakuDanDanSource',\n      danmakuFontWeight = 'danmakuFontWeight',\n      danmakuFollowSpeed = 'danmakuFollowSpeed',\n      themeMode = 'themeMode',\n      themeColor = 'themeColor',\n      privateMode = 'privateMode',\n      autoPlay = 'autoPlay',\n      autoPlayNext = 'autoPlayNext',\n      playResume = 'playResume',\n      showPlayerError = 'showPlayerError',\n      oledEnhance = 'oledEnhance',\n      displayMode = 'displayMode',\n      enableGitProxy = 'enableGitProxy',\n      enableSystemProxy = 'enableSystemProxy',\n      defaultStartupPage = 'defaultStartupPage',\n      /// Deprecated\n      isWideScreen = 'isWideScreen',\n      webDavEnable = 'webDavEnable',\n      webDavEnableHistory = 'webDavEnableHistory',\n      webDavEnableCollect = 'webDavEnableCollect',\n      webDavURL = 'webDavURL',\n      webDavUsername = 'webDavUsername',\n      webDavPassword = 'webDavPasswd',\n      lowMemoryMode = 'lowMemoryMode',\n      showWindowButton = 'showWindowButton',\n      useDynamicColor = 'useDynamicColor',\n      exitBehavior = 'exitBehavior',\n      playerDebugMode = 'playerDebugMode',\n      syncPlayEndPoint = 'syncPlayEndPoint',\n      androidEnableOpenSLES = 'androidEnableOpenSLES',\n      androidVideoRenderer = 'androidVideoRenderer',\n      defaultSuperResolutionType = 'defaultSuperResolutionType',\n      superResolutionWarn = 'superResolutionWarn',\n      playerDisableAnimations = 'playerDisableAnimations',\n      playerLogLevel = 'playerLogLevel',\n      searchNotShowWatchedBangumis = 'searchNotShowWatchedBangumis',\n      searchNotShowAbandonedBangumis = 'searchNotShowAbandonedBangumis',\n      timelineNotShowAbandonedBangumis = 'timelineNotShowAbandonedBangumis',\n      timelineNotShowWatchedBangumis = 'timelineNotShowWatchedBangumis',\n      useSystemFont = 'useSystemFont',\n      forceAdBlocker = 'forceAdBlocker',\n      proxyEnable = 'proxyEnable',\n      proxyConfigured = 'proxyConfigured',\n      proxyUrl = 'proxyUrl',\n      proxyTestUrl = 'proxyTestUrl',\n      showRating = 'showRating',\n      downloadParallelEpisodes = 'downloadParallelEpisodes',\n      downloadParallelSegments = 'downloadParallelSegments',\n      downloadDanmaku = 'downloadDanmaku';\n}\n"
  },
  {
    "path": "lib/utils/string_match.dart",
    "content": "// 计算两个字符串的编辑距离, 曾用于弹幕标题匹配\n// 由于 DanDanPlay 现在直接提供基于 bgmBangumiID 的弹幕反查，此方法已不再使用\n\nimport 'dart:math';\n\nint levenshteinDistance(String s1, String s2) {\n  if (s1 == s2) return 0;\n  if (s1.isEmpty) return s2.length;\n  if (s2.isEmpty) return s1.length;\n\n  List<int> v0 = List<int>.generate(s2.length + 1, (i) => i);\n  List<int> v1 = List<int>.filled(s2.length + 1, 0);\n\n  for (int i = 0; i < s1.length; i++) {\n    v1[0] = i + 1;\n\n    for (int j = 0; j < s2.length; j++) {\n      int cost = (s1[i] == s2[j]) ? 0 : 1;\n      v1[j + 1] = min(v1[j] + 1, min(v0[j + 1] + 1, v0[j] + cost));\n    }\n\n    for (int j = 0; j < v0.length; j++) {\n      v0[j] = v1[j];\n    }\n  }\n\n  return v1[s2.length];\n}\n\n// 计算相似度百分比\ndouble calculateSimilarity(String s1, String s2) {\n  int maxLength = max(s1.length, s2.length);\n  if (maxLength == 0) return 1.0;\n  if (s1 == s2) return 1.0;\n  return (1.0 - levenshteinDistance(s1, s2) / maxLength);\n}\n"
  },
  {
    "path": "lib/utils/syncplay.dart",
    "content": "// https://syncplay.pl/about/protocol/\n\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nconst double PING_MOVING_AVERAGE_WEIGHT = 0.85;\n\nclass SyncplayException implements Exception {\n  final String message;\n  SyncplayException(this.message);\n}\n\nclass SyncplayConnectionException extends SyncplayException {\n  SyncplayConnectionException(super.message);\n}\n\nclass SyncplayProtocolException extends SyncplayException {\n  SyncplayProtocolException(super.message);\n}\n\nabstract class SyncplayMessage {\n  Map<String, dynamic> toJson();\n}\n\nclass HelloMessage extends SyncplayMessage {\n  final String username;\n  final String version;\n  final String room;\n\n  HelloMessage({\n    required this.username,\n    required this.version,\n    required this.room,\n  });\n\n  @override\n  Map<String, dynamic> toJson() => {\n        'Hello': {\n          'username': username,\n          'room': {\n            'name': room,\n          },\n          'version': version,\n          'features': {\n            'sharedPlaylists': true,\n            'chat': true,\n            'featureList': true,\n            'readiness': true,\n            'managedRooms': false,\n          }\n        },\n      };\n}\n\nclass StateMessage extends SyncplayMessage {\n  final double position;\n  final bool paused;\n  final bool? doSeek;\n  final String? setBy;\n\n  // syncplay controll message\n  final int? clientAck;\n  final int? serverAck;\n\n  // latency calculation\n  double clientLatencyCalculation;\n  double? latencyCalculation;\n  final double clientRtt;\n\n  StateMessage({\n    required this.position,\n    required this.paused,\n    this.setBy,\n    this.doSeek,\n    this.clientAck,\n    this.serverAck,\n    required this.clientLatencyCalculation,\n    this.latencyCalculation,\n    this.clientRtt = 0.0,\n  });\n\n  @override\n  Map<String, dynamic> toJson() => {\n        'State': {\n          if (clientAck != null || serverAck != null)\n            'ignoringOnTheFly': {\n              if (clientAck != null) 'client': clientAck,\n              if (serverAck != null) 'server': serverAck,\n            },\n          'ping': {\n            'clientRtt': clientRtt,\n            'clientLatencyCalculation': clientLatencyCalculation,\n            if (latencyCalculation != null)\n              'latencyCalculation': latencyCalculation,\n          },\n          'playstate': {\n            'position': position,\n            'paused': paused,\n            if (setBy != null) 'setBy': setBy,\n            'doSeek': doSeek,\n          },\n        },\n      };\n}\n\nclass SetMessage extends SyncplayMessage {\n  final double? duration;\n  final String? fileName;\n  final String? username;\n  final int? size;\n  final String? setBy;\n  final String? room;\n  final bool? setJoined;\n  final bool? setReady;\n\n  SetMessage({\n    this.duration,\n    this.fileName,\n    this.username,\n    this.size,\n    this.setBy,\n    this.room,\n    this.setJoined,\n    this.setReady,\n  });\n\n  @override\n  Map<String, dynamic> toJson() {\n    if (setJoined != null && room != null && username != null) {\n      return {\n        \"Set\": {\n          room: {\n            \"room\": {\"name\": room},\n            \"event\": {\"joined\": true}\n          },\n        }\n      };\n    }\n    if (setReady != null) {\n      return {\n        'Set': {\n          \"ready\": {\"isReady\": true, \"manuallyInitiated\": false}\n        }\n      };\n    }\n    return {\n      'Set': {\n        if (fileName != null)\n          'file': {\n            'duration': duration,\n            'name': fileName,\n            'size': size,\n          },\n        if (room != null)\n          \"user\": {\n            setBy: {\n              \"room\": {\"name\": room},\n            },\n          },\n      },\n    };\n  }\n}\n\nclass ChatMessage extends SyncplayMessage {\n  final String message;\n\n  ChatMessage({\n    required this.message,\n  });\n\n  @override\n  Map<String, dynamic> toJson() => {'Chat': message};\n}\n\nclass TLSMessage extends SyncplayMessage {\n  final String message;\n\n  TLSMessage({\n    required this.message,\n  });\n\n  @override\n  Map<String, dynamic> toJson() => {\n        'TLS': {\n          'startTLS': message,\n        },\n      };\n}\n\nclass SyncplayClient {\n  final String _host;\n  final int _port;\n  bool _isTLS = false;\n  Socket? _socket;\n  String? _username;\n  String? _currentRoom;\n  String? _currentFileName;\n  double _currentPositon = 0.0;\n  bool _isPaused = true;\n  StreamController<Map<String, dynamic>>? _generalMessageController =\n      StreamController.broadcast();\n  StreamController<Map<String, dynamic>>? _roomMessageController =\n      StreamController.broadcast();\n  StreamController<Map<String, dynamic>>? _chatMessageController =\n      StreamController.broadcast();\n  StreamController<Map<String, dynamic>>? _flieChangedMessageController =\n      StreamController.broadcast();\n  StreamController<Map<String, dynamic>>? _positionChangedMessageController =\n      StreamController.broadcast();\n  double? _lastLatencyCalculation;\n\n  // Network status\n  double _clientRtt = 0.0;\n  double _serverRtt = 0.0;\n  double _avrRtt = 0.0;\n  double _fd = 0.0;\n\n  // IgnoringOnTheFly\n  int _clientIgnoringOnTheFly = 0;\n  int _serverIgnoringOnTheFly = 0;\n\n  bool get isConnected => _socket != null;\n  bool get isTLS => _isTLS;\n  String? get username => _username;\n  String? get currentRoom => _currentRoom;\n  String? get currentFileName => _currentFileName;\n  double get clientRtt => _clientRtt;\n  double get serverRtt => _serverRtt;\n  double get avgRtt => _avrRtt;\n  double get fd => _fd;\n\n  Stream<Map<String, dynamic>> get onGeneralMessage {\n    _generalMessageController ??= StreamController.broadcast();\n    return _generalMessageController!.stream;\n  }\n\n  Stream<Map<String, dynamic>> get onRoomMessage {\n    _roomMessageController ??= StreamController.broadcast();\n    return _roomMessageController!.stream;\n  }\n\n  Stream<Map<String, dynamic>> get onChatMessage {\n    _chatMessageController ??= StreamController.broadcast();\n    return _chatMessageController!.stream;\n  }\n\n  Stream<Map<String, dynamic>> get onFileChangedMessage {\n    _flieChangedMessageController ??= StreamController.broadcast();\n    return _flieChangedMessageController!.stream;\n  }\n\n  Stream<Map<String, dynamic>> get onPositionChangedMessage {\n    _positionChangedMessageController ??= StreamController.broadcast();\n    return _positionChangedMessageController!.stream;\n  }\n\n  SyncplayClient({required String host, required int port})\n      : _host = host,\n        _port = port;\n\n  Future<void> connect({bool enableTLS = true}) async {\n    if (_generalMessageController?.isClosed ?? true) {\n      _generalMessageController = StreamController.broadcast();\n    }\n    if (_flieChangedMessageController?.isClosed ?? true) {\n      _flieChangedMessageController = StreamController.broadcast();\n    }\n    if (_positionChangedMessageController?.isClosed ?? true) {\n      _positionChangedMessageController = StreamController.broadcast();\n    }\n    try {\n      await _socket?.close();\n      _socket = null;\n      print('SyncPlay: connecting to Syncplay server: $_host:$_port');\n      _socket = await Socket.connect(_host, _port);\n      print('SyncPlay: connected to Syncplay server: $_host:$_port');\n      _setupSocketHandlers();\n      if (enableTLS) {\n        requestTLS();\n      }\n    } on SocketException catch (e) {\n      _generalMessageController?.addError(\n        SyncplayConnectionException(\n            'SyncPlay: connection failed: ${e.message}'),\n      );\n    }\n  }\n\n  Future<void> requestTLS() async {\n    print('SyncPlay: requesting TLS connection upgrade');\n    await _sendMessage(TLSMessage(message: 'send'));\n  }\n\n  Future<void> joinRoom(String room, String username) async {\n    print('SyncPlay: joining room: $room as $username');\n    await _sendMessage(HelloMessage(\n      username: username,\n      version: '1.7.0',\n      room: room,\n    ));\n  }\n\n  Future<void> sendChatMessage(String message) async {\n    if (_currentRoom == null || _username == null) {\n      _generalMessageController?.addError(\n        SyncplayProtocolException(\n            'SyncPlay: send chat message failed, not in a room'),\n      );\n      return;\n    }\n    await _sendMessage(ChatMessage(\n      message: message,\n    ));\n  }\n\n  Future<void> setSyncPlayPlaying(\n      String bangumiName, double duration, int size) async {\n    if (_currentRoom == null || _username == null) {\n      _generalMessageController?.addError(\n        SyncplayProtocolException(\n            'SyncPlay: set playing bangumi failed, not in a room'),\n      );\n      return;\n    }\n    await _sendMessage(SetMessage(\n        duration: duration,\n        fileName: bangumiName,\n        size: size,\n        setBy: _username ?? '',\n        room: _currentRoom ?? ''));\n  }\n\n  Future<void> sendSyncPlaySyncRequest({bool? doSeek}) async {\n    _sendState(\n      position: _currentPositon,\n      paused: _isPaused,\n      doSeek: doSeek,\n      stateChange: true,\n    );\n  }\n\n  Future<void> disconnect() async {\n    print('SyncPlay: disconnecting from Syncplay server: $_host:$_port');\n    await _generalMessageController?.close();\n    _generalMessageController = null;\n    await _roomMessageController?.close();\n    _roomMessageController = null;\n    await _chatMessageController?.close();\n    _chatMessageController = null;\n    await _flieChangedMessageController?.close();\n    _flieChangedMessageController = null;\n    await _positionChangedMessageController?.close();\n    _positionChangedMessageController = null;\n    try {\n      await _socket?.close();\n    } catch (_) {}\n    _socket = null;\n    _currentRoom = null;\n    _username = null;\n    _currentFileName = null;\n    _currentPositon = 0.0;\n    _isPaused = true;\n    _isTLS = false;\n    _lastLatencyCalculation = null;\n    _clientIgnoringOnTheFly = 0;\n    _serverIgnoringOnTheFly = 0;\n    _clientRtt = 0.0;\n    _serverRtt = 0.0;\n    _avrRtt = 0.0;\n    _fd = 0.0;\n  }\n\n  void setPosition(double position) {\n    _currentPositon = position;\n  }\n\n  void setPaused(bool paused) {\n    _isPaused = paused;\n  }\n\n  void _setupSocketHandlers() {\n    String buffer = '';\n\n    _socket?.listen(\n      (data) {\n        final dataStr = utf8.decode(data);\n        buffer += dataStr;\n        while (true) {\n          final startIndex = buffer.indexOf('{');\n          if (startIndex == -1) {\n            break;\n          }\n\n          int braceCount = 0;\n          int? endIndex;\n          for (int i = startIndex; i < buffer.length; i++) {\n            if (buffer[i] == '{') {\n              braceCount++;\n            } else if (buffer[i] == '}') {\n              braceCount--;\n              if (braceCount == 0) {\n                endIndex = i;\n                break;\n              }\n            }\n          }\n          if (endIndex == null) break;\n\n          final jsonStr = buffer.substring(startIndex, endIndex + 1);\n          try {\n            // print(\n            //     'SyncPlay: [${DateTime.now().millisecondsSinceEpoch / 1000.0}] received message: $jsonStr');\n            _handleMessage(json.decode(jsonStr));\n          } catch (e) {\n            _generalMessageController?.addError(\n              SyncplayProtocolException(\n                  'SyncPlay: received data parse failed: $e'),\n            );\n          }\n          buffer = buffer.substring(endIndex + 1);\n        }\n      },\n      onError: (error) => _generalMessageController?.addError(\n        SyncplayConnectionException('SyncPlay: socket error: $error'),\n      ),\n      onDone: () => _generalMessageController?.addError(\n        SyncplayConnectionException('SyncPlay: connection closed'),\n      ),\n    );\n  }\n\n  void _handleMessage(dynamic data) async {\n    final json = data as Map<String, dynamic>;\n    if (json.containsKey('TLS')) {\n      if (json['TLS'].containsKey('startTLS')) {\n        if (json['TLS']['startTLS'] == 'true') {\n          var plainSocket = _socket;\n          try {\n            _socket = await SecureSocket.secure(plainSocket!);\n            _setupSocketHandlers();\n            _isTLS = true;\n            print('SyncPlay: TLS connection established');\n            try {\n              plainSocket.close();\n            } catch (_) {}\n          } catch (e) {\n            print('SyncPlay: TLS connection upgrade failed: $e');\n            _socket = plainSocket;\n            _isTLS = false;\n          }\n        }\n      }\n      return;\n    }\n    if (json.containsKey('Hello')) {\n      if (json['Hello'].containsKey('room') &&\n          json['Hello']['room'].containsKey('name')) {\n        _username = json['Hello']['username'];\n        _currentRoom = json['Hello']['room']['name'];\n        print(\n            'SyncPlay: joined room: $_currentRoom as $_username, version: ${json['Hello']['version']}');\n        _setReady();\n      }\n      _generalMessageController?.add({\n        'username': json['Hello']['username'],\n        'room': json['Hello']['room']['name'],\n      });\n      return;\n    }\n    if (json.containsKey('State')) {\n      if (json['State'].containsKey('ping')) {\n        _lastLatencyCalculation =\n            json['State']['ping']['latencyCalculation']?.toDouble();\n        if (json['State']['ping'].containsKey('serverRtt')) {\n          _serverRtt = json['State']['ping']['serverRtt']?.toDouble() ?? 0.0;\n        }\n        _updateClientRttAndFd(\n            json['State'][\"ping\"][\"clientLatencyCalculation\"], _serverRtt);\n      }\n      if (json['State'].containsKey('ignoringOnTheFly')) {\n        var ignoringOnTheFly = json['State']['ignoringOnTheFly'];\n        if (ignoringOnTheFly.containsKey('server')) {\n          _serverIgnoringOnTheFly = ignoringOnTheFly['server'];\n          _clientIgnoringOnTheFly = 0;\n        } else if (ignoringOnTheFly.containsKey('client')) {\n          if (ignoringOnTheFly['client'] == _clientIgnoringOnTheFly) {\n            _clientIgnoringOnTheFly = 0;\n          }\n        }\n      }\n      if (_clientIgnoringOnTheFly == 0) {\n        _currentPositon = (json['State']['playstate']['paused'] ?? true)\n            ? (json['State']['playstate']['position']?.toDouble() ?? 0.0)\n            : ((json['State']['playstate']['position']?.toDouble() ?? 0.0) +\n                _fd);\n        _isPaused = json['State']['playstate']['paused'] ?? true;\n        _positionChangedMessageController?.add({\n          'calculatedPositon': (json['State']['playstate']['paused'] ?? true)\n              ? (json['State']['playstate']['position']?.toDouble() ?? 0.0)\n              : ((json['State']['playstate']['position']?.toDouble() ?? 0.0) +\n                  _fd),\n          'position': json['State']['playstate']['position']?.toDouble() ?? 0.0,\n          'paused': json['State']['playstate']['paused'] ?? true,\n          'doSeek': json['State']['playstate']['doSeek'] ?? false,\n          'setBy': json['State']['playstate']['setBy'] ?? '',\n          'clientRtt': _clientRtt,\n          'serverRtt': _serverRtt,\n          'avrRtt': _avrRtt,\n          'fd': _fd,\n        });\n      }\n      _sendState(\n        position: _currentPositon,\n        paused: _isPaused,\n      );\n      return;\n    }\n    if (json.containsKey('Set')) {\n      if (json['Set'].containsKey('playlistIndex')) {\n        _roomMessageController?.add({\n          'type': 'init',\n          'username': json['Set']['playlistIndex']['user'] ?? '',\n        });\n        return;\n      }\n      if (json['Set'].containsKey('user')) {\n        Map<String, dynamic> userMap = data['Set']['user'];\n        userMap.forEach((username, details) {\n          if (!details.containsKey('event')) {\n            return;\n          }\n          var event = details['event'].keys.first ?? 'unknown';\n          _roomMessageController?.add({\n            'type': event,\n            'username': username,\n          });\n        });\n        for (var username in userMap.keys) {\n          var userData = userMap[username];\n          if (userData is Map && userData.containsKey('file')) {\n            var fileData = userData['file'];\n            var fileName = fileData['name'];\n            _currentFileName = fileName;\n            _flieChangedMessageController?.add({\n              'name': fileName,\n              'setBy': username,\n            });\n          }\n        }\n      }\n      return;\n    }\n    if (json.containsKey('Chat')) {\n      if (json['Chat'].containsKey('message') &&\n          json['Chat'].containsKey('username')) {\n        _chatMessageController?.add({\n          'message': json['Chat']['message'],\n          'username': json['Chat']['username'],\n        });\n      }\n      return;\n    }\n    _generalMessageController?.addError(\n      SyncplayProtocolException('SyncPlay: unknown message type'),\n    );\n  }\n\n  Future<void> _setReady() async {\n    if (_currentRoom == null || _username == null) {\n      _generalMessageController?.addError(\n        SyncplayProtocolException('SyncPlay: set ready failed, not in a room'),\n      );\n      return;\n    }\n    await _sendMessage(\n      SetMessage(\n        setJoined: true,\n        username: _username,\n        room: _currentRoom,\n      ),\n    );\n    await _sendMessage(\n      SetMessage(\n        setReady: true,\n      ),\n    );\n  }\n\n  Future<void> _sendMessage(SyncplayMessage message) async {\n    if (_socket == null) {\n      _generalMessageController?.addError(\n        SyncplayConnectionException('SyncPlay: not connected to server'),\n      );\n      return;\n    }\n    final json = message.toJson();\n    final jsonStr = jsonEncode(json);\n    // print(\n    //     'SyncPlay: [${DateTime.now().millisecondsSinceEpoch / 1000.0}] sending message: $jsonStr');\n    _socket?.write('$jsonStr\\r\\n');\n  }\n\n  void _sendState(\n      {double? position,\n      bool? paused,\n      bool? doSeek,\n      bool stateChange = false}) {\n    int? clientArck;\n    int? serverAck;\n    if (stateChange) {\n      _clientIgnoringOnTheFly = _clientIgnoringOnTheFly + 1;\n    }\n    if (_serverIgnoringOnTheFly > 0) {\n      serverAck = _serverIgnoringOnTheFly;\n      _serverIgnoringOnTheFly = 0;\n    }\n    if (_clientIgnoringOnTheFly > 0) {\n      clientArck = _clientIgnoringOnTheFly;\n    }\n    _sendMessage(StateMessage(\n      position: position ?? _currentPositon,\n      paused: paused ?? _isPaused,\n      latencyCalculation: _lastLatencyCalculation,\n      clientLatencyCalculation: DateTime.now().millisecondsSinceEpoch / 1000.0,\n      clientRtt: _clientRtt,\n      setBy: _username,\n      clientAck: clientArck,\n      serverAck: serverAck,\n      doSeek: doSeek,\n    ));\n  }\n\n  void _updateClientRttAndFd(double? timestamp, double senderRtt) {\n    if (timestamp == null) return;\n\n    // Calculate RTT: current time minus the passed timestamp\n    double newClientRtt =\n        DateTime.now().millisecondsSinceEpoch / 1000.0 - timestamp;\n\n    // If the new RTT is less than 0, it means the server is not responding\n    if (newClientRtt < 0 || senderRtt < 0) return;\n    _clientRtt = newClientRtt;\n\n    // If it's the first time calculating, initialize the average RTT\n    if (_avrRtt == 0) {\n      _avrRtt = _clientRtt;\n    }\n\n    // Use moving average to update RTT, smooth the delay data\n    _avrRtt = _avrRtt * PING_MOVING_AVERAGE_WEIGHT +\n        _clientRtt * (1 - PING_MOVING_AVERAGE_WEIGHT);\n\n    // Calculate the forward delay based on the sender's RTT\n    if (senderRtt < _clientRtt) {\n      _fd = _avrRtt / 2 + (_clientRtt - senderRtt);\n    } else {\n      _fd = _avrRtt / 2;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/utils/syncplay_endpoint.dart",
    "content": "class SyncPlayEndPoint {\n  final String host;\n  final int port;\n\n  const SyncPlayEndPoint({required this.host, required this.port});\n}\n\nSyncPlayEndPoint? parseSyncPlayEndPoint(String endPoint) {\n  final input = endPoint.trim();\n  if (input.isEmpty) {\n    return null;\n  }\n\n  String host = '';\n  String portStr = '';\n\n  if (input.startsWith('[')) {\n    final closeIndex = input.indexOf(']');\n    if (closeIndex == -1) {\n      return null;\n    }\n    host = input.substring(1, closeIndex);\n    final rest = input.substring(closeIndex + 1);\n    if (!rest.startsWith(':')) {\n      return null;\n    }\n    portStr = rest.substring(1);\n  } else {\n    final lastColonIndex = input.lastIndexOf(':');\n    if (lastColonIndex == -1) {\n      return null;\n    }\n    host = input.substring(0, lastColonIndex);\n    portStr = input.substring(lastColonIndex + 1);\n  }\n\n  host = host.trim();\n  portStr = portStr.trim();\n  if (host.isEmpty || portStr.isEmpty) {\n    return null;\n  }\n\n  final port = int.tryParse(portStr);\n  if (port == null || port <= 0 || port > 65535) {\n    return null;\n  }\n\n  return SyncPlayEndPoint(host: host, port: port);\n}\n"
  },
  {
    "path": "lib/utils/timed_shutdown_service.dart",
    "content": "import 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:kazumi/bean/dialog/dialog_helper.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass TimedShutdownService {\n  static final TimedShutdownService _instance = TimedShutdownService._internal();\n  factory TimedShutdownService() => _instance;\n  TimedShutdownService._internal();\n\n  Timer? _shutdownTimer;\n  int _remainingSeconds = 0;\n  bool _isDialogShowing = false;\n  /// Last set minutes, used for repeat functionality\n  int _lastSetMinutes = 0;\n\n  /// Callback to invoke when timer expires (e.g., pause video)\n  VoidCallback? _onExpiredCallback;\n  \n  /// Remaining time in seconds notifier\n  final ValueNotifier<int> remainingSecondsNotifier = ValueNotifier<int>(0);\n  \n  /// Currently set minutes notifier (for UI display)\n  final ValueNotifier<int> setMinutesNotifier = ValueNotifier<int>(0);\n\n  /// Whether a shutdown timer is currently active\n  bool get isActive => _shutdownTimer != null && _shutdownTimer!.isActive;\n\n  /// Currently set minutes (0 = disabled)\n  int get setMinutes => setMinutesNotifier.value;\n  \n  /// Remaining time in seconds\n  int get remainingSeconds => remainingSecondsNotifier.value;\n\n  /// Start the shutdown timer with the given duration in minutes\n  /// [onExpired] callback is invoked when timer expires (before showing dialog)\n  void start(int minutes, {VoidCallback? onExpired}) {\n    cancel();\n    if (minutes <= 0) return;\n\n    _lastSetMinutes = minutes;\n    _remainingSeconds = minutes * 60;\n    remainingSecondsNotifier.value = _remainingSeconds;\n    setMinutesNotifier.value = minutes;\n    _onExpiredCallback = onExpired;\n    \n    // Update remaining time every second (runs globally, not tied to playback)\n    _shutdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {\n      if (_remainingSeconds > 0) {\n        _remainingSeconds--;\n        remainingSecondsNotifier.value = _remainingSeconds;\n      }\n      \n      if (_remainingSeconds <= 0) {\n        timer.cancel();\n        _shutdownTimer = null;\n        _onTimerExpired();\n      }\n    });\n  }\n\n  /// Repeat the timer with the last set duration\n  void repeat() {\n    if (_lastSetMinutes > 0) {\n      start(_lastSetMinutes, onExpired: _onExpiredCallback);\n    }\n  }\n\n  /// Cancel the current shutdown timer\n  void cancel() {\n    _shutdownTimer?.cancel();\n    _shutdownTimer = null;\n    _remainingSeconds = 0;\n    _onExpiredCallback = null;\n    if (remainingSecondsNotifier.value != 0) {\n      remainingSecondsNotifier.value = 0;\n    }\n    if (setMinutesNotifier.value != 0) {\n      setMinutesNotifier.value = 0;\n    }\n    \n    // If dialog is showing, dismiss it\n    if (_isDialogShowing) {\n      KazumiDialog.dismiss();\n      _isDialogShowing = false;\n    }\n  }\n\n  /// Called when timer expires: invoke callback and show dialog\n  void _onTimerExpired() {\n    // Reset UI state so it doesn't show 00:00\n    setMinutesNotifier.value = 0;\n    \n    // Invoke the callback if set (e.g., pause video)\n    try {\n      _onExpiredCallback?.call();\n    } catch (e) {\n      KazumiLogger().e('TimedShutdownService: onExpired callback failed', error: e);\n    }\n    \n    _showTimerExpiredDialog();\n  }\n\n  /// Show the timer expired dialog with repeat/close options\n  void _showTimerExpiredDialog() {\n    if (_isDialogShowing) return;\n    _isDialogShowing = true;\n\n    KazumiDialog.show(\n      clickMaskDismiss: false,\n      onDismiss: () {\n        _isDialogShowing = false;\n      },\n      builder: (context) {\n        return AlertDialog(\n          title: const Text('定时关闭'),\n          content: const Text('定时时间已到，视频已暂停'),\n          actions: [\n            TextButton(\n              onPressed: () {\n                _isDialogShowing = false;\n                KazumiDialog.dismiss();\n                repeat();\n                KazumiDialog.showToast(message: '已重新开始 $_lastSetMinutes 分钟定时');\n              },\n              child: const Text('重复'),\n            ),\n            TextButton(\n              onPressed: () {\n                _isDialogShowing = false;\n                KazumiDialog.dismiss();\n              },\n              child: Text(\n                '关闭',\n                style: TextStyle(color: Theme.of(context).colorScheme.outline),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  /// Format remaining seconds to a readable string (e.g., \"15:30\")\n  String formatRemainingTime() {\n    int totalSeconds = remainingSecondsNotifier.value;\n    if (totalSeconds <= 0) return '00:00';\n    final minutes = totalSeconds ~/ 60;\n    final seconds = totalSeconds % 60;\n    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';\n  }\n\n  /// Format minutes to readable display string (e.g., \"1 小时 30 分钟\")\n  String formatMinutesToDisplay(int totalMinutes) {\n    final hours = totalMinutes ~/ 60;\n    final minutes = totalMinutes % 60;\n    if (hours > 0 && minutes > 0) {\n      return '$hours 小时 $minutes 分钟';\n    } else if (hours > 0) {\n      return '$hours 小时';\n    } else {\n      return '$minutes 分钟';\n    }\n  }\n\n  /// Show custom timer picker dialog and start timer if user confirms\n  /// Uses KazumiDialog to avoid context-related resource leaks\n  /// [onExpired] callback is invoked when timer expires (before showing dialog)\n  static void showCustomTimerDialog({\n    String title = '自定义定时',\n    bool autoStart = true,\n    VoidCallback? onExpired,\n    void Function(int)? onResult,\n  }) {\n    int selectedHours = 0;\n    int selectedMinutes = 0;\n\n    KazumiDialog.show(\n      builder: (context) {\n        return StatefulBuilder(\n          builder: (context, setState) {\n            return AlertDialog(\n              title: Text(title),\n              content: SizedBox(\n                height: 200,\n                child: Row(\n                  children: [\n                    // Hours picker\n                    Expanded(\n                      child: Column(\n                        children: [\n                          const Text('时', style: TextStyle(fontSize: 14)),\n                          const SizedBox(height: 8),\n                          Expanded(\n                            child: CupertinoPicker(\n                              scrollController: FixedExtentScrollController(initialItem: selectedHours),\n                              itemExtent: 40,\n                              onSelectedItemChanged: (index) {\n                                setState(() => selectedHours = index);\n                              },\n                              children: List.generate(25, (index) => Center(\n                                child: Text(index.toString().padLeft(2, '0'), style: const TextStyle(fontSize: 20)),\n                              )),\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                    const Text(':', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),\n                    // Minutes picker\n                    Expanded(\n                      child: Column(\n                        children: [\n                          const Text('分', style: TextStyle(fontSize: 14)),\n                          const SizedBox(height: 8),\n                          Expanded(\n                            child: CupertinoPicker(\n                              scrollController: FixedExtentScrollController(initialItem: selectedMinutes),\n                              itemExtent: 40,\n                              onSelectedItemChanged: (index) {\n                                setState(() => selectedMinutes = index);\n                              },\n                              children: List.generate(60, (index) => Center(\n                                child: Text(index.toString().padLeft(2, '0'), style: const TextStyle(fontSize: 20)),\n                              )),\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              actions: [\n                TextButton(\n                  onPressed: () => KazumiDialog.dismiss(),\n                  child: Text(\n                    '取消',\n                    style: TextStyle(color: Theme.of(context).colorScheme.outline),\n                  ),\n                ),\n                TextButton(\n                  onPressed: () {\n                    final totalMinutes = selectedHours * 60 + selectedMinutes;\n                    if (totalMinutes <= 0) {\n                      KazumiDialog.showToast(message: '请选择有效的时间');\n                      return;\n                    }\n                    KazumiDialog.dismiss();\n                    if (autoStart) {\n                      TimedShutdownService().start(totalMinutes, onExpired: onExpired);\n                      KazumiDialog.showToast(\n                        message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(totalMinutes)} 后定时关闭',\n                      );\n                    }\n                    onResult?.call(totalMinutes);\n                  },\n                  child: const Text('确定'),\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/utils.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:dio/dio.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:kazumi/modules/danmaku/danmaku_module.dart';\nimport 'package:kazumi/request/api.dart';\nimport 'package:kazumi/utils/constants.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/mortis.dart';\nimport 'package:path/path.dart' as path;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart';\n\nclass Utils {\n  static final Random random = Random();\n\n  static bool? _isDocumentStartScriptSupported;\n\n  /// 检查 Android WebView 是否支持 DOCUMENT_START_SCRIPT 特性\n  static Future<void> checkWebViewFeatureSupport() async {\n    if (Platform.isAndroid) {\n      _isDocumentStartScriptSupported = await PlatformWebViewFeature.static()\n          .isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT);\n    }\n  }\n\n  static bool get isDocumentStartScriptSupported =>\n      _isDocumentStartScriptSupported ?? false;\n\n  static Future<bool> isLowResolution() async {\n    if (Platform.isMacOS) {\n      return false;\n    }\n    Map<String, double> screenInfo = await getScreenInfo();\n    if (screenInfo['height']! / screenInfo['ratio']! < 900) {\n      return true;\n    }\n    return false;\n  }\n\n  static String getRandomUA() {\n    final random = Random();\n    String randomElement =\n        userAgentsList[random.nextInt(userAgentsList.length)];\n    return randomElement;\n  }\n\n  static String getRandomAcceptedLanguage() {\n    final random = Random();\n    String randomElement =\n        acceptLanguageList[random.nextInt(acceptLanguageList.length)];\n    return randomElement;\n  }\n\n  static Future<Map<String, double>> getScreenInfo() async {\n    final MediaQueryData mediaQuery = MediaQueryData.fromView(\n        WidgetsBinding.instance.platformDispatcher.views.first);\n    final Size screenSize =\n        WidgetsBinding.instance.platformDispatcher.displays.first.size;\n    final double screenRatio = mediaQuery.devicePixelRatio;\n    Map<String, double>? screenInfo = {};\n    screenInfo = {\n      'width': screenSize.width,\n      'height': screenSize.height,\n      'ratio': screenRatio\n    };\n    return screenInfo;\n  }\n\n  // 从URL参数中解析 m3u8/mp4\n  static String decodeVideoSource(String iframeUrl) {\n    var decodedUrl = Uri.decodeFull(iframeUrl);\n    RegExp regExp = RegExp(r'(http[s]?://.*?\\.m3u8)|(http[s]?://.*?\\.mp4)',\n        caseSensitive: false);\n\n    Uri uri = Uri.parse(decodedUrl);\n    Map<String, String> params = uri.queryParameters;\n\n    String matchedUrl = iframeUrl;\n    params.forEach((key, value) {\n      if (regExp.hasMatch(value)) {\n        matchedUrl = value;\n        return;\n      }\n    });\n\n    return Uri.encodeFull(matchedUrl);\n  }\n\n  // 完全相对时间显示\n  static String formatTimestampToRelativeTime(timeStamp) {\n    var difference = DateTime.now()\n        .difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000));\n\n    if (difference.inDays > 365) {\n      return '${difference.inDays ~/ 365}年前';\n    } else if (difference.inDays > 30) {\n      return '${difference.inDays ~/ 30}个月前';\n    } else if (difference.inDays > 0) {\n      return '${difference.inDays}天前';\n    } else if (difference.inHours > 0) {\n      return '${difference.inHours}小时前';\n    } else if (difference.inMinutes > 0) {\n      return '${difference.inMinutes}分钟前';\n    } else {\n      return '刚刚';\n    }\n  }\n\n  // 时间显示，刚刚，x分钟前\n  static String dateFormat(timeStamp, {formatType = 'list'}) {\n    // 当前时间\n    int time = (DateTime.now().millisecondsSinceEpoch / 1000).round();\n    // 对比\n    int distance = (time - timeStamp).toInt();\n    // 当前年日期\n    String currentYearStr = 'MM月DD日 hh:mm';\n    String lastYearStr = 'YY年MM月DD日 hh:mm';\n    if (formatType == 'detail') {\n      currentYearStr = 'MM-DD hh:mm';\n      lastYearStr = 'YY-MM-DD hh:mm';\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: lastYearStr,\n          toInt: false,\n          formatType: formatType);\n    }\n    if (distance <= 60) {\n      return '刚刚';\n    } else if (distance <= 3600) {\n      return '${(distance / 60).floor()}分钟前';\n    } else if (distance <= 43200) {\n      return '${(distance / 60 / 60).floor()}小时前';\n    } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year ==\n        DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) {\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: currentYearStr,\n          toInt: false,\n          formatType: formatType);\n    } else {\n      return CustomStamp_str(\n          timestamp: timeStamp,\n          date: lastYearStr,\n          toInt: false,\n          formatType: formatType);\n    }\n  }\n\n  // 时间戳转时间\n  static String CustomStamp_str(\n      {int? timestamp, // 为空则显示当前时间\n      String? date, // 显示格式，比如：'YY年MM月DD日 hh:mm:ss'\n      bool toInt = true, // 去除0开头\n      String? formatType}) {\n    timestamp ??= (DateTime.now().millisecondsSinceEpoch / 1000).round();\n    String timeStr =\n        (DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)).toString();\n\n    dynamic dateArr = timeStr.split(' ')[0];\n    dynamic timeArr = timeStr.split(' ')[1];\n\n    String YY = dateArr.split('-')[0];\n    String MM = dateArr.split('-')[1];\n    String DD = dateArr.split('-')[2];\n\n    String hh = timeArr.split(':')[0];\n    String mm = timeArr.split(':')[1];\n    String ss = timeArr.split(':')[2];\n\n    ss = ss.split('.')[0];\n\n    // 去除0开头\n    if (toInt) {\n      MM = (int.parse(MM)).toString();\n      DD = (int.parse(DD)).toString();\n      hh = (int.parse(hh)).toString();\n      mm = (int.parse(mm)).toString();\n    }\n\n    if (date == null) {\n      return timeStr;\n    }\n\n    // if (formatType == 'list' && int.parse(DD) > DateTime.now().day - 2) {\n    //   return '昨天';\n    // }\n\n    date = date\n        .replaceAll('YY', YY)\n        .replaceAll('MM', MM)\n        .replaceAll('DD', DD)\n        .replaceAll('hh', hh)\n        .replaceAll('mm', mm)\n        .replaceAll('ss', ss);\n    if (int.parse(YY) == DateTime.now().year &&\n        int.parse(MM) == DateTime.now().month) {\n      // 当天\n      if (int.parse(DD) == DateTime.now().day) {\n        return '今天';\n      }\n    }\n    return date;\n  }\n\n  static String makeHeroTag(v) {\n    return v.toString() + random.nextInt(9999).toString();\n  }\n\n  // 版本对比\n  static bool needUpdate(localVersion, remoteVersion) {\n    List<String> localVersionList = localVersion.split('.');\n    List<String> remoteVersionList = remoteVersion.split('.');\n    for (int i = 0; i < localVersionList.length; i++) {\n      int localVersion = int.parse(localVersionList[i]);\n      int remoteVersion = int.parse(remoteVersionList[i]);\n      if (remoteVersion > localVersion) {\n        return true;\n      } else if (remoteVersion < localVersion) {\n        return false;\n      }\n    }\n    return false;\n  }\n\n  // 日期字符串转换为 weekday (eg: 2024-09-23 -> 1 (星期一))\n  static int dateStringToWeekday(String dateString) {\n    try {\n      DateTime date = DateTime.parse(dateString);\n      return date.weekday;\n    } catch (_) {\n      return 1;\n    }\n  }\n\n  static String jsonToKazumiBase64(String jsonStr) {\n    String base64Str = base64Encode(utf8.encode(jsonStr));\n    return 'kazumi://$base64Str';\n  }\n\n  static String kazumiBase64ToJson(String kazumiBase64Str) {\n    if (!kazumiBase64Str.startsWith('kazumi://')) {\n      return '';\n    }\n    String base64Str = kazumiBase64Str.substring(9);\n    String jsonStr = utf8.decode(base64.decode(base64Str));\n    return jsonStr;\n  }\n\n  static String durationToString(Duration duration) {\n    String pad(int n) => n.toString().padLeft(2, '0');\n    var hours = pad(duration.inHours % 24);\n    var minutes = pad(duration.inMinutes % 60);\n    var seconds = pad(duration.inSeconds % 60);\n    if (hours == \"00\") {\n      return \"$minutes:$seconds\";\n    } else {\n      return \"$hours:$minutes:$seconds\";\n    }\n  }\n\n  static Future<String> latest() async {\n    try {\n      var resp = await Dio().get<Map<String, dynamic>>(Api.latestApp);\n      if (resp.data?.containsKey(\"tag_name\") ?? false) {\n        return resp.data![\"tag_name\"];\n      } else {\n        throw resp.data?[\"message\"];\n      }\n    } catch (e) {\n      return Api.version;\n    }\n  }\n\n  static oledDarkTheme(ThemeData defaultDarkTheme) {\n    return defaultDarkTheme.copyWith(\n      scaffoldBackgroundColor: Colors.black,\n      colorScheme: defaultDarkTheme.colorScheme.copyWith(\n        onPrimary: Colors.black,\n        onSecondary: Colors.black,\n        // background: Colors.black,\n        // onBackground: Colors.black,\n        surface: Colors.black,\n        onSurface: Colors.white,\n      ),\n    );\n  }\n\n  static generateDanmakuColor(int colorValue) {\n    // 提取颜色分量\n    int red = (colorValue >> 16) & 0xFF;\n    int green = (colorValue >> 8) & 0xFF;\n    int blue = colorValue & 0xFF;\n    // 创建Color对象\n    Color color = Color.fromARGB(255, red, green, blue);\n    return color;\n  }\n\n  static List<Danmaku> mergeDuplicateDanmakus(\n    List<Danmaku> danmakus, {\n      double timeWindowSeconds = 0,\n    }) {\n    final Map<String, List<Danmaku>> grouped = {};\n\n    // 弹幕规范化处理\n    // 去首尾空格并小写，全角转半角，去掉所有空白、标点符号，连续重复内容压缩，保留日语字符\n    for (var d in danmakus) {\n      String text = d.message;\n\n      text = text.trim().toLowerCase();\n\n      final buffer = StringBuffer();\n      for (int i = 0; i < text.length; i++) {\n        int code = text.codeUnitAt(i);\n        if (code == 0x3000) {\n          buffer.writeCharCode(0x20);\n        } else if (code >= 0xFF01 && code <= 0xFF5E) {\n          buffer.writeCharCode(code - 0xFEE0);\n        } else {\n          buffer.writeCharCode(code);\n        }\n      }\n      text = buffer.toString();\n\n      text = text.replaceAll(RegExp(r'\\s+'), '');\n\n      text = text.replaceAll(RegExp(\n        r'[^\\w\\u4e00-\\u9fff\\u3040-\\u309F\\u30A0-\\u30FF\\u31F0-\\u31FF\\uFF65-\\uFF9F]',\n        unicode: true,\n      ),'');\n\n      text = text.replaceAllMapped(RegExp(r'(.)\\1{2,}'), (match) {\n        final char = match.group(1)!;\n        return char * 3;\n      });\n\n      grouped.putIfAbsent(text, () => []);\n      grouped[text]!.add(d);\n    }\n\n    final List<Danmaku> result = [];\n\n    grouped.forEach((normalized, list) {\n      if (list.isEmpty) return;\n\n      if (timeWindowSeconds <= 0) {\n        if (list.length == 1) {\n          result.add(list.first);\n        } else {\n          result.add(\n            Danmaku(\n              message: '${list.first.message} x${list.length}',\n              time: list.first.time, // 默认取第一条\n              type: 5,\n              color: Utils.generateDanmakuColor(0xFFFFFF), // 白色弹幕\n              source: list.first.source,\n            ),\n          );\n        }\n        return;\n      }\n\n      list.sort((a, b) => a.time.compareTo(b.time));\n\n      List<Danmaku> currentGroup = [];\n      for (var item in list) {\n        if (currentGroup.isEmpty) {\n          currentGroup.add(item);\n          continue;\n        }\n        final last = currentGroup.last;\n        if ((item.time - last.time) <= timeWindowSeconds) {\n          currentGroup.add(item);\n        } else {\n          if (currentGroup.length == 1) {\n            result.add(currentGroup.first);\n          } else {\n            result.add(\n              Danmaku(\n                message: '${currentGroup.first.message} x${currentGroup.length}',\n                time: currentGroup.first.time,\n                type: 5,\n                color: Utils.generateDanmakuColor(0xFFFFFF),\n                source: currentGroup.first.source,\n              ),\n            );\n          }\n          currentGroup = [item];\n        }\n      }\n\n      if (currentGroup.isNotEmpty) {\n        if (currentGroup.length == 1) {\n          result.add(currentGroup.first);\n        } else {\n          result.add(\n            Danmaku(\n              message: '${currentGroup.first.message} x${currentGroup.length}',\n              time: currentGroup.first.time,\n              type: 5,\n              color: Utils.generateDanmakuColor(0xFFFFFF),\n              source: currentGroup.first.source,\n            ),\n          );\n        }\n      }\n    });\n\n    return result;\n  }\n\n  static int extractEpisodeNumber(String input) {\n    RegExp regExp = RegExp(r'第?(\\d+)[话集]?');\n    Match? match = regExp.firstMatch(input);\n\n    if (match != null && match.group(1) != null) {\n      return int.tryParse(match.group(1)!) ?? 0;\n    }\n\n    return 0;\n  }\n\n  /// 判断是否为桌面设备\n  static bool isDesktop() {\n    return Platform.isWindows || Platform.isMacOS || Platform.isLinux;\n  }\n\n  /// 判断设备是否为宽屏\n  static bool isWideScreen() {\n    final MediaQueryData mediaQuery = MediaQueryData.fromView(\n        WidgetsBinding.instance.platformDispatcher.views.first);\n    final bool isWideScreen = mediaQuery.size.shortestSide >= 600 &&\n        mediaQuery.size.shortestSide / mediaQuery.size.longestSide >= 9 / 16;\n    return isWideScreen;\n  }\n\n  /// 判断设备是否为平板\n  static bool isTablet() {\n    return isWideScreen() && !isDesktop();\n  }\n\n  /// 判断设备是否需要紧凑布局\n  static bool isCompact() {\n    return !isDesktop() && !isWideScreen();\n  }\n\n  /// 判断是否分屏模式 (android only)\n  static Future<bool> isInMultiWindowMode() async {\n    if (Platform.isAndroid) {\n      const platform = MethodChannel('com.predidit.kazumi/intent');\n      try {\n        final bool result =\n            await platform.invokeMethod('checkIfInMultiWindowMode');\n        return result;\n      } on PlatformException catch (e) {\n        print(\"Failed to check multi window mode: '${e.message}'.\");\n        return false;\n      }\n    }\n    return false;\n  }\n\n  /// 判定是否运行在X11环境下 (Linux only)\n  static Future<bool> isRunningOnX11() async {\n    if (Platform.isLinux) {\n      const platform = MethodChannel('com.predidit.kazumi/intent');\n      try {\n        final bool result = await platform.invokeMethod('isRunningOnX11');\n        return result;\n      } on PlatformException catch (e) {\n        print(\"Failed to check X11 environment: '${e.message}'.\");\n        return false;\n      }\n    }\n    return false;\n  }\n\n  // Deprecated\n  static Future<void> enterWindowsFullscreen() async {\n    if (Platform.isWindows) {\n      const platform = MethodChannel('com.predidit.kazumi/intent');\n      try {\n        await platform.invokeMethod('enterFullscreen');\n      } on PlatformException catch (e) {\n        print(\"Failed to enter native window mode: '${e.message}'.\");\n      }\n    }\n  }\n\n  // Deprecated\n  static Future<void> exitWindowsFullscreen() async {\n    if (Platform.isWindows) {\n      const platform = MethodChannel('com.predidit.kazumi/intent');\n      try {\n        await platform.invokeMethod('exitFullscreen');\n      } on PlatformException catch (e) {\n        print(\"Failed to exit native window mode: '${e.message}'.\");\n      }\n    }\n  }\n\n  // 进入全屏显示\n  static Future<void> enterFullScreen({bool lockOrientation = true}) async {\n    // if (Platform.isWindows) {\n    //   await enterWindowsFullscreen();\n    //   return;\n    // }\n    if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {\n      await windowManager.setFullScreen(true);\n      return;\n    }\n    await SystemChrome.setEnabledSystemUIMode(\n      SystemUiMode.immersiveSticky,\n    );\n    if (!lockOrientation) {\n      return;\n    }\n    if (Platform.isAndroid) {\n      bool isInMultiWindowMode = await Utils.isInMultiWindowMode();\n      if (isInMultiWindowMode) {\n        return;\n      }\n    }\n    await landScape();\n  }\n\n  static Future<int> getAndroidSdkVersion() async {\n    if (Platform.isAndroid) {\n      const platform = MethodChannel('com.predidit.kazumi/intent');\n      try {\n        final int sdkVersion =\n            await platform.invokeMethod('getAndroidSdkVersion');\n        return sdkVersion;\n      } on PlatformException catch (e) {\n        KazumiLogger().e(\"Failed to get Android SDK version: '${e.message}'.\");\n        return 0;\n      }\n    }\n    return 0;\n  }\n\n  //退出全屏显示\n  static Future<void> exitFullScreen({bool lockOrientation = true}) async {\n    // if (Platform.isWindows) {\n    //   await exitWindowsFullscreen();\n    // }\n    if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {\n      await windowManager.setFullScreen(false);\n    }\n    late SystemUiMode mode = SystemUiMode.edgeToEdge;\n    try {\n      if (Platform.isAndroid || Platform.isIOS) {\n        if (Platform.isAndroid) {\n          const platform = MethodChannel('com.predidit.kazumi/intent');\n          try {\n            final int sdkVersion =\n                await platform.invokeMethod('getAndroidSdkVersion');\n            if (sdkVersion < 29) {\n              mode = SystemUiMode.manual;\n            }\n          } on PlatformException catch (e) {\n            KazumiLogger()\n                .e(\"Failed to get Android SDK version: '${e.message}'.\");\n          }\n        }\n        await SystemChrome.setEnabledSystemUIMode(\n          mode,\n          overlays: SystemUiOverlay.values,\n        );\n        if (Utils.isCompact() && lockOrientation) {\n          if (Platform.isAndroid) {\n            bool isInMultiWindowMode = await Utils.isInMultiWindowMode();\n            if (isInMultiWindowMode) {\n              return;\n            }\n          }\n          verticalScreen();\n        }\n      }\n    } catch (exception, stacktrace) {\n      KazumiLogger().e('DisPlay: failed to exit full screen',\n          error: exception, stackTrace: stacktrace);\n    }\n  }\n\n  //横屏\n  static Future<void> landScape() async {\n    dynamic document;\n    try {\n      if (kIsWeb) {\n        await document.documentElement?.requestFullscreen();\n      } else if (Platform.isAndroid || Platform.isIOS) {\n        await SystemChrome.setPreferredOrientations(\n          [\n            DeviceOrientation.landscapeLeft,\n            DeviceOrientation.landscapeRight,\n          ],\n        );\n      }\n    } catch (exception, stacktrace) {\n      KazumiLogger().e('Display: failed to enter landscape mode',\n          error: exception, stackTrace: stacktrace);\n    }\n  }\n\n  //竖屏\n  static Future<void> verticalScreen() async {\n    await SystemChrome.setPreferredOrientations([\n      DeviceOrientation.portraitUp,\n    ]);\n  }\n\n  // 解除屏幕旋转限制\n  static Future<void> unlockScreenRotation() async {\n    await SystemChrome.setPreferredOrientations([]);\n  }\n\n  static String getSeasonStringByMonth(int month) {\n    if (month <= 3) return '冬';\n    if (month <= 6) return '春';\n    if (month <= 9) return '夏';\n    return '秋';\n  }\n\n  // 进入桌面设备小窗模式\n  static Future<void> enterDesktopPIPWindow() async {\n    await windowManager.setAlwaysOnTop(true);\n    await windowManager.setSize(const Size(480, 270));\n  }\n\n  // 退出桌面设备小窗模式\n  static Future<void> exitDesktopPIPWindow() async {\n    bool isLowResolution = await Utils.isLowResolution();\n    await windowManager.setAlwaysOnTop(false);\n    await windowManager.setSize(\n        isLowResolution ? const Size(800, 600) : const Size(1280, 860));\n    await windowManager.center();\n  }\n\n  static bool isSameSeason(DateTime d1, DateTime d2) {\n    return d1.year == d2.year && (d1.month - d2.month).abs() <= 2;\n  }\n\n  static Future<String> getPlayerTempPath() async {\n    final directory = await getTemporaryDirectory();\n    return directory.path;\n  }\n\n  static String buildShadersAbsolutePath(\n      String baseDirectory, List<String> shaders) {\n    List<String> absolutePaths = shaders.map((shader) {\n      return path.join(baseDirectory, shader);\n    }).toList();\n    if (Platform.isWindows) {\n      return absolutePaths.join(';');\n    }\n    return absolutePaths.join(':');\n  }\n\n  static String generateDandanSignature(String path, int timestamp) {\n    String id = mortis['id']!;\n    String value = mortis['value']!;\n    String data = id + timestamp.toString() + path + value;\n    var bytes = utf8.encode(data);\n    var digest = sha256.convert(bytes);\n    return base64Encode(digest.bytes);\n  }\n\n  /// 格式化日期\n  /// eg: 2025-07-27T09:14:12Z -> 2025-07-27\n  static String formatDate(String dateString) {\n    try {\n      final date = DateTime.parse(dateString);\n      return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';\n    } catch (e) {\n      return dateString;\n    }\n  }\n\n  /// 计算文件的 SHA256 哈希值\n  static Future<String> calculateFileHash(File file) async {\n    final bytes = await file.readAsBytes();\n    final digest = sha256.convert(bytes);\n    return digest.toString();\n  }\n\n  /// 销毁播放器菜单\n  static Future<void> disposePlayerMenu() async {\n    if (!Platform.isMacOS) return; //暂时只适配macOS\n    const MethodChannel appmenu = MethodChannel(\"com.predidit.kazumi/appmenu\");\n    await appmenu.invokeMethod(\"setMenuEnabled\", {\n      \"menu\": \"PlayerMenu\",\n      \"enable\": false,\n    });\n  }\n\n  /// 初始化播放器菜单\n  static Future<void> initPlayerMenu(\n      Map<String, void Function()> actions) async {\n    if (!Platform.isMacOS) return; //暂时只适配macOS\n    const MethodChannel appmenu = MethodChannel(\"com.predidit.kazumi/appmenu\");\n    await appmenu.invokeMethod(\"setMenuEnabled\", {\n      \"menu\": \"PlayerMenu\",\n      \"enable\": true,\n    });\n    appmenu.setMethodCallHandler((call) async {\n      final action = actions[call.method];\n      action?.call();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/utils/webdav.dart",
    "content": "import 'dart:io';\nimport 'package:webdav_client/webdav_client.dart' as webdav;\nimport 'package:hive_ce/hive.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/modules/collect/collect_module.dart';\nimport 'package:kazumi/modules/collect/collect_change_module.dart';\n\nclass WebDav {\n  late String webDavURL;\n  late String webDavUsername;\n  late String webDavPassword;\n  late Directory webDavLocalTempDirectory;\n  late webdav.Client client;\n\n  bool initialized = false;\n  // make sure only one upload history task at a time\n  bool isHistorySyncing = false;\n\n  WebDav._internal();\n  static final WebDav _instance = WebDav._internal();\n  factory WebDav() => _instance;\n\n  Future<void> init() async {\n    var directory = await getApplicationSupportDirectory();\n    webDavLocalTempDirectory = Directory('${directory.path}/webdavTemp');\n    Box setting = GStorage.setting;\n    webDavURL = setting.get(SettingBoxKey.webDavURL, defaultValue: '');\n    webDavUsername =\n        setting.get(SettingBoxKey.webDavUsername, defaultValue: '');\n    webDavPassword =\n        setting.get(SettingBoxKey.webDavPassword, defaultValue: '');\n    if (webDavURL.isEmpty) {\n      //KazumiLogger().log(Level.warning, 'WebDAV URL is not set');\n      throw Exception('请先填写WebDAV URL');\n    }\n    client = webdav.newClient(\n      webDavURL,\n      user: webDavUsername,\n      password: webDavPassword,\n      debug: false,\n    );\n    client.setHeaders({'accept-charset': 'utf-8'});\n    try {\n      await client.ping();\n      try {\n        // KazumiLogger().log(Level.warning, 'webDav backup directory not exists, creating');\n        await client.mkdir('/kazumiSync');    \n        if (!await webDavLocalTempDirectory.exists()) {\n          await webDavLocalTempDirectory.create(recursive: true);\n        }\n        initialized = true;\n        KazumiLogger().i('WebDav: webDav backup directory create success');\n      } catch (_) {\n        KazumiLogger().e('WebDav: webDav backup directory create failed');\n        rethrow;\n      }\n    } catch (e) {\n      KazumiLogger().e('WebDav: WebDAV ping failed', error: e);\n      rethrow;\n    }\n  }\n\n  Future<void> update(String boxName) async {\n    var directory = await getApplicationSupportDirectory();\n    final localFilePath = '${directory.path}/hive/$boxName.hive'; \n    final tempFilePath = '${webDavLocalTempDirectory.path}/$boxName.tmp';\n    final webDavPath = '/kazumiSync/$boxName.tmp';\n    await File(localFilePath)\n          .copy(tempFilePath);\n    try {\n      await client.remove('$webDavPath.cache');\n    } catch (_) {}\n    await client.writeFromFile(tempFilePath,\n        '$webDavPath.cache', onProgress: (c, t) {\n      // print(c / t);\n    });\n    try {\n      await client.remove(webDavPath);\n    } catch (_) {\n      KazumiLogger().w('WebDav: former backup file not exist');\n    }\n    await client.rename(\n        '$webDavPath.cache', webDavPath, true);\n    try {\n      await File(tempFilePath).delete();\n    } catch (_) {}\n  }\n\n  Future<void> updateHistory() async {\n    if (isHistorySyncing) {\n      KazumiLogger().w('WebDav: History is currently syncing');\n      throw Exception('History is currently syncing');\n    }\n    isHistorySyncing = true;\n    try {\n      await update('histories');\n    } catch (e) {\n      KazumiLogger().e('WebDav: update history failed', error: e);\n      rethrow;\n    } finally {\n      isHistorySyncing = false;\n    }\n  }\n\n  Future<void> updateCollectibles() async {\n    // don't try muliti thread update here\n    // some webdav server may not support muliti thread write\n    // you will get 423 locked error\n    try {\n      await update('collectibles');\n      if (GStorage.collectChanges.isNotEmpty) {\n        await update('collectchanges');\n      }\n    } catch (e) {\n      KazumiLogger().e('WebDav: update collectibles failed', error: e);\n      rethrow;\n    }\n  }\n\n  Future<void> download(String boxName) async {\n    String fileName = '$boxName.tmp';\n    final existingFile = File('${webDavLocalTempDirectory.path}/$fileName');\n    if (await existingFile.exists()) {\n      await existingFile.delete();\n    }\n    await client.read2File('/kazumiSync/$fileName', existingFile.path,\n        onProgress: (c, t) {\n      // print(c / t);\n    });\n  }\n\n  Future<void> downloadAndPatchHistory() async {\n    if (isHistorySyncing) {\n      KazumiLogger().w('WebDav: History is currently syncing');\n      throw Exception('History is currently syncing');\n    }\n    isHistorySyncing = true;\n    String fileName = 'histories.tmp';\n    try {\n      final existingFile = File('${webDavLocalTempDirectory.path}/$fileName');\n      await download('histories');\n      await GStorage.patchHistory(existingFile.path);\n    } catch (e) {\n      KazumiLogger()\n          .e('WebDav: download and patch history failed', error: e);\n      rethrow;\n    } finally {\n      isHistorySyncing = false;\n    }\n  }\n\n  Future<void> syncCollectibles() async {\n    List<CollectedBangumi> remoteCollectibles = [];\n    List<CollectedBangumiChange> remoteChanges = [];\n\n    final files = await client.readDir('/kazumiSync');\n    final collectiblesExists = files.any((file) => file.name == 'collectibles.tmp');\n    final changesExists = files.any((file) => file.name == 'collectchanges.tmp');\n    if (!collectiblesExists && !changesExists) {\n      await updateCollectibles();\n      return;\n    }\n    \n    List<Future<void>> downloadFutures = [];\n    if (collectiblesExists) {\n      downloadFutures.add(download('collectibles').catchError((e) {\n        KazumiLogger().e('WebDav: download collectibles failed', error: e);\n        throw Exception('WebDav: download collectibles failed');\n      }));\n    }\n    if (changesExists) {\n      downloadFutures.add(download('collectchanges').catchError((e) {\n        KazumiLogger().e('WebDav: download collectchanges failed', error: e);\n        throw Exception('WebDav: download collectchanges failed');\n      }));\n    }\n    if (downloadFutures.isNotEmpty) {\n      await Future.wait(downloadFutures);\n    } \n    try {\n      if (collectiblesExists) {\n        remoteCollectibles = await GStorage.getCollectiblesFromFile(\n          '${webDavLocalTempDirectory.path}/collectibles.tmp');\n      }\n      if (changesExists) {\n        remoteChanges = await GStorage.getCollectChangesFromFile(\n          '${webDavLocalTempDirectory.path}/collectchanges.tmp');\n      }  \n    } catch (e) {\n      KazumiLogger().e('WebDav: get collectibles failed', error: e);\n      throw Exception('WebDav: get collectibles from file failed'); \n    }\n    if (remoteChanges.isNotEmpty || remoteCollectibles.isNotEmpty) {\n      await GStorage.patchCollectibles(remoteCollectibles, remoteChanges);\n    }\n    await updateCollectibles();\n  }\n\n  Future<void> ping() async {\n    try {\n      await client.ping();\n    } catch (e) {\n      KazumiLogger().e('WebDav: WebDav ping failed', error: e);\n      rethrow;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/webview/captcha/captcha_webview_controller.dart",
    "content": "import 'dart:io';\nimport 'dart:async';\n\nimport 'package:kazumi/webview/captcha/impl/captcha_webview_inappwebview_impl.dart';\nimport 'package:kazumi/webview/captcha/impl/captcha_webview_windows_impl.dart';\nimport 'package:kazumi/webview/captcha/impl/captcha_webview_linux_impl.dart';\n\nabstract class CaptchaWebviewController<T> {\n  /// Webview controller\n  T? webviewController;\n\n  /// For type-1 (captcha image), whether a captcha image has been detected,\n  /// used to determine if verification is successful after page navigation\n  bool captchaWasFound = false;\n\n  /// For type-2 (auto-click button), we set this flag when the button click is triggered.\n  /// Then on page navigation or DOM change, if this flag is set,\n  /// we can confirm verification success without relying solely on captcha disappearance.\n  bool buttonWasClicked = false;\n\n  final StreamController<String> captchaImageFoundController =\n      StreamController<String>.broadcast();\n  final StreamController<void> captchaDisappearedController =\n      StreamController<void>.broadcast();\n  final StreamController<bool> initEventController =\n      StreamController<bool>.broadcast();\n  final StreamController<String> logEventController =\n      StreamController<String>.broadcast();\n\n  /// WebView 初始化完成事件\n  Stream<bool> get onInitialized => initEventController.stream;\n\n  /// 验证码图片 src 找到时触发（携带图片绝对 URL）\n  Stream<String> get onCaptchaImageFound => captchaImageFoundController.stream;\n\n  /// 验证码图片从页面消失时触发\n  Stream<void> get onCaptchaDisappeared => captchaDisappearedController.stream;\n\n  /// 调试日志\n  Stream<String> get onLog => logEventController.stream;\n\n  /// 初始化 WebView\n  Future<void> init();\n\n  /// 加载指定 URL，并注入监听验证码图片的 JS 脚本（类型1：图片验证码）\n  ///\n  /// [url] 要加载的页面地址（一般为搜索 URL）\n  /// [captchaXpath] 验证码图片元素的 XPath 选择器\n  /// [inputXpath] 可选，验证码输入框的 XPath。如果提供，会在检测验证码前先触发输入框的 focus 事件（某些站点需要）\n  Future<void> loadPage(String url, String captchaXpath, {String? inputXpath});\n\n  /// 加载指定 URL，并注入监听验证按钮的 JS 脚本（类型2：自动点击验证按钮）\n  ///\n  /// 检测到 [buttonXpath] 元素后立即模拟点击；按钮消失时触发 [onCaptchaDisappeared]。\n  /// [url] 要加载的页面地址\n  /// [buttonXpath] 验证按钮元素的 XPath 选择器\n  Future<void> loadPageForButtonClick(String url, String buttonXpath);\n\n  /// 在 WebView 内通过 JS 模拟输入验证码并模拟点击提交按钮\n  ///\n  /// [captchaCode] 用户输入的验证码文本\n  /// [inputXpath]  验证码输入框元素的 XPath\n  /// [buttonXpath] 提交按钮元素的 XPath\n  Future<void> submitCaptchaInteract(\n      String captchaCode, String inputXpath, String buttonXpath);\n\n  /// 获取当前页面的 Cookie 字符串（\"key1=val1; key2=val2\"）\n  ///\n  /// [pageUrl] 当前加载的页面地址，部分平台用于精确过滤 Cookie\n  Future<String> getCookieString(String pageUrl);\n\n  /// 卸载当前页面（跳转到 about:blank）\n  Future<void> unloadPage();\n\n  /// 释放 WebView 资源\n  void dispose();\n}\n\nclass CaptchaWebviewControllerFactory {\n  static CaptchaWebviewController getController() {\n    if (Platform.isWindows) {\n      return CaptchaWebviewWindowsImpl();\n    }\n    if (Platform.isLinux) {\n      return CaptchaWebviewLinuxImpl();\n    }\n    // Android, iOS, macOS\n    return CaptchaWebviewInAppWebviewImpl();\n  }\n}\n"
  },
  {
    "path": "lib/webview/captcha/impl/captcha_webview_inappwebview_impl.dart",
    "content": "import 'dart:async';\n\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/webview/captcha/captcha_webview_controller.dart';\nimport 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart';\n\nclass CaptchaWebviewInAppWebviewImpl\n    extends CaptchaWebviewController<PlatformInAppWebViewController> {\n  PlatformHeadlessInAppWebView? _headlessWebView;\n  bool _handlersRegistered = false;\n  String _currentCaptchaImageXpath = '';\n  String _currentInputXpath = '';\n\n  @override\n  Future<void> init() async {\n    _headlessWebView ??= PlatformHeadlessInAppWebView(\n      PlatformHeadlessInAppWebViewCreationParams(\n        initialSettings: InAppWebViewSettings(\n          userAgent: Utils.getRandomUA(),\n          mediaPlaybackRequiresUserGesture: true,\n          cacheEnabled: true,\n          blockNetworkImage: false,\n          loadsImagesAutomatically: true,\n          upgradeKnownHostsToHTTPS: false,\n          safeBrowsingEnabled: false,\n        ),\n        onWebViewCreated: (controller) {\n          logEventController.add('[Captcha WebView] Created');\n          webviewController = controller;\n          initEventController.add(true);\n        },\n        onLoadStart: (controller, url) {\n          logEventController.add('[Captcha WebView] Load start: $url');\n        },\n        onLoadStop: (controller, url) {\n          logEventController.add('[Captcha WebView] Load stop: $url');\n          if (buttonWasClicked && !captchaDisappearedController.isClosed) {\n            KazumiLogger().i('[Captcha WebView] Button click → page navigated, verification done');\n            buttonWasClicked = false;\n            captchaDisappearedController.add(null);\n          }\n        },\n        onReceivedError: (controller, request, error) {\n          logEventController\n              .add('[Captcha WebView] Error: ${error.description}');\n        },\n      ),\n    );\n    await _headlessWebView!.run();\n  }\n\n  void _registerHandlers() {\n    if (_handlersRegistered) return;\n\n    webviewController?.addJavaScriptHandler(\n      handlerName: 'CaptchaImageBridge',\n      callback: (args) {\n        final src = args.isNotEmpty ? args[0].toString() : '';\n        logEventController.add('[Captcha WebView] Captcha image found: $src');\n        if (src.isNotEmpty && !captchaImageFoundController.isClosed) {\n          captchaWasFound = true;\n          captchaImageFoundController.add(src);\n        }\n      },\n    );\n\n    webviewController?.addJavaScriptHandler(\n      handlerName: 'CaptchaStatusBridge',\n      callback: (args) {\n        final status = args.isNotEmpty ? args[0].toString() : '';\n        logEventController.add('[Captcha WebView JS] Page captcha status: $status');\n        if (status == 'absent' && captchaWasFound &&\n            !captchaDisappearedController.isClosed) {\n          KazumiLogger().i('[Captcha WebView] Captcha gone after navigation (StatusBridge)');\n          captchaWasFound = false;\n          captchaDisappearedController.add(null);\n        }\n      },\n    );\n\n    webviewController?.addJavaScriptHandler(\n      handlerName: 'CaptchaGoneBridge',\n      callback: (args) {\n        logEventController.add('[Captcha WebView] Captcha image disappeared');\n        buttonWasClicked = false;\n        if (!captchaDisappearedController.isClosed) {\n          captchaDisappearedController.add(null);\n        }\n      },\n    );\n\n    webviewController?.addJavaScriptHandler(\n      handlerName: 'ButtonClickedBridge',\n      callback: (args) {\n        logEventController.add('[Captcha WebView] Button clicked flag set');\n        buttonWasClicked = true;\n      },\n    );\n\n    webviewController?.addJavaScriptHandler(\n      handlerName: 'CaptchaLogBridge',\n      callback: (args) {\n        if (args.isNotEmpty) {\n          logEventController.add('[Captcha WebView JS] ${args[0]}');\n        }\n      },\n    );\n\n    _handlersRegistered = true;\n    logEventController.add('[Captcha WebView] JS handlers registered');\n  }\n\n  Future<void> _addCaptchaUserScript() async {\n    if (_currentCaptchaImageXpath.isEmpty) return;\n\n    final escapedXpath =\n        _currentCaptchaImageXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedInputXpath =\n        _currentInputXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n\n    // Remove any previously injected captcha script before adding a fresh one.\n    await webviewController?.removeAllUserScripts();\n\n    const String scriptTemplate = \"\"\"\nwindow.flutter_inappwebview.callHandler('CaptchaLogBridge',\n  'CaptchaScript loaded on: ' + window.location.href);\n\nvar _captchaXpath = '{XPATH}';\nvar _inputXpath = '{INPUT_XPATH}';\nvar _captchaPoller = null;\nvar _disappearObserver = null;\n\nfunction _evalXpath() {\n  try {\n    var result = document.evaluate(\n      _captchaXpath, document, null,\n      XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n    return result.singleNodeValue;\n  } catch(e) { return null; }\n}\n\nfunction _startDisappearMonitor() {\n  if (_disappearObserver) return;\n  _disappearObserver = new MutationObserver(function() {\n    var node = _evalXpath();\n    if (!node) {\n      _disappearObserver.disconnect();\n      _disappearObserver = null;\n      try {\n        window.flutter_inappwebview.callHandler('CaptchaGoneBridge', '');\n      } catch(e) {}\n    }\n  });\n  _disappearObserver.observe(document.documentElement,\n    { childList: true, subtree: true, attributes: true });\n}\n\nfunction _captureAsBase64(imgNode, callback) {\n  function doCapture() {\n    try {\n      var canvas = document.createElement('canvas');\n      canvas.width = imgNode.naturalWidth || imgNode.width || 100;\n      canvas.height = imgNode.naturalHeight || imgNode.height || 40;\n      var ctx = canvas.getContext('2d');\n      ctx.drawImage(imgNode, 0, 0);\n      callback(canvas.toDataURL('image/png'));\n    } catch(e) { callback(null); }\n  }\n  if (imgNode.complete && imgNode.naturalWidth > 0) {\n    doCapture();\n  } else {\n    imgNode.addEventListener('load', doCapture);\n    imgNode.addEventListener('error', function() { callback(null); });\n  }\n}\n\nfunction _checkForCaptcha() {\n  var node = _evalXpath();\n  if (node) {\n    _captureAsBase64(node, function(dataUrl) {\n      if (dataUrl) {\n        try {\n          window.flutter_inappwebview.callHandler('CaptchaImageBridge', dataUrl);\n        } catch(e) {}\n      }\n    });\n    _startDisappearMonitor();\n    return true;\n  }\n  return false;\n}\n\nfunction _triggerInputFocus() {\n  if (!_inputXpath) return false;\n  try {\n    var inputResult = document.evaluate(_inputXpath, document, null,\n      XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n    var inputEl = inputResult.singleNodeValue;\n    if (inputEl) {\n      if (typeof \\$ !== 'undefined' && \\$) {\n        \\$(inputEl).trigger('focus');\n        return true;\n      } else if (typeof jQuery !== 'undefined' && jQuery) {\n        jQuery(inputEl).trigger('focus');\n        return true;\n      } else {\n        inputEl.focus();\n        return true;\n      }\n    }\n  } catch(e) {\n    try { window.flutter_inappwebview.callHandler('CaptchaLogBridge',\n      'Failed to trigger input focus - ' + e.message); } catch(e2) {}\n  }\n  return false;\n}\n\n// Report captcha status to Dart at DOMContentLoaded so that after a full-page\n// navigation Dart can detect that verification succeeded (captcha is gone).\n// Also trigger input focus here since DOM is ready at this point.\nwindow.addEventListener('DOMContentLoaded', function() {\n  _triggerInputFocus();\n  var node = _evalXpath();\n  try {\n    window.flutter_inappwebview.callHandler('CaptchaStatusBridge', node ? 'present' : 'absent');\n  } catch(e) {}\n});\n\nif (!_checkForCaptcha()) {\n  _captchaPoller = setInterval(function() {\n    if (_checkForCaptcha()) {\n      clearInterval(_captchaPoller);\n      _captchaPoller = null;\n    }\n  }, 500);\n}\n\"\"\";\n\n    final script = scriptTemplate\n        .replaceAll('{XPATH}', escapedXpath)\n        .replaceAll('{INPUT_XPATH}', escapedInputXpath);\n    await webviewController?.addUserScripts(\n      userScripts: [\n        UserScript(\n          source: script,\n          injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,\n        ),\n      ],\n    );\n  }\n\n  @override\n  Future<void> loadPage(String url, String captchaXpath, {String? inputXpath}) async {\n    _currentCaptchaImageXpath = captchaXpath;\n    _currentInputXpath = inputXpath ?? '';\n    captchaWasFound = false;\n    buttonWasClicked = false;\n    _registerHandlers();\n    await _addCaptchaUserScript();\n    try {\n      await PlatformCookieManager(const PlatformCookieManagerCreationParams())\n          .deleteAllCookies();\n      logEventController.add('[Captcha WebView] Cookies cleared before load');\n    } catch (_) {}\n    await webviewController\n        ?.loadUrl(urlRequest: URLRequest(url: WebUri(url)));\n  }\n\n  @override\n  Future<void> loadPageForButtonClick(String url, String buttonXpath) async {\n    _currentCaptchaImageXpath = ''; // disable captcha-image script on navigation\n    captchaWasFound = false;\n    buttonWasClicked = false;\n    _registerHandlers();\n    await _addButtonClickUserScript(buttonXpath);\n    try {\n      await PlatformCookieManager(const PlatformCookieManagerCreationParams())\n          .deleteAllCookies();\n      logEventController.add('[Captcha WebView] Cookies cleared before load');\n    } catch (_) {}\n    await webviewController\n        ?.loadUrl(urlRequest: URLRequest(url: WebUri(url)));\n  }\n\n  Future<void> _addButtonClickUserScript(String buttonXpath) async {\n    final escapedXpath =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    await webviewController?.removeAllUserScripts();\n\n    const String scriptTemplate = \"\"\"\ntry { window.flutter_inappwebview.callHandler('CaptchaLogBridge',\n  'ButtonClickScript loaded on: ' + window.location.href); } catch(e) {}\n\nvar _btnXpath = '{XPATH}';\nvar _clicked = false;\nvar _poller = null;\nvar _disappearObserver = null;\n\nfunction _evalBtnXpath() {\n  try {\n    var result = document.evaluate(\n      _btnXpath, document, null,\n      XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n    return result.singleNodeValue;\n  } catch(e) { return null; }\n}\n\nfunction _startDisappearMonitor() {\n  if (_disappearObserver) return;\n  _disappearObserver = new MutationObserver(function() {\n    if (!_evalBtnXpath()) {\n      _disappearObserver.disconnect();\n      _disappearObserver = null;\n      try { window.flutter_inappwebview.callHandler('CaptchaGoneBridge', ''); } catch(e) {}\n    }\n  });\n  _disappearObserver.observe(document.documentElement,\n    { childList: true, subtree: true, attributes: true });\n}\n\nfunction _checkAndClick() {\n  var btn = _evalBtnXpath();\n  if (btn && !_clicked) {\n    _clicked = true;\n    btn.click();\n    try { window.flutter_inappwebview.callHandler('ButtonClickedBridge', ''); } catch(e) {}\n    _startDisappearMonitor();\n    return true;\n  }\n  return false;\n}\n\nif (!_checkAndClick()) {\n  _poller = setInterval(function() {\n    if (_checkAndClick()) { clearInterval(_poller); _poller = null; }\n  }, 500);\n}\n\"\"\";\n\n    final script = scriptTemplate.replaceAll('{XPATH}', escapedXpath);\n    await webviewController?.addUserScripts(\n      userScripts: [\n        UserScript(\n          source: script,\n          injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,\n        ),\n      ],\n    );\n  }\n\n  @override\n  Future<void> submitCaptchaInteract(\n      String captchaCode, String inputXpath, String buttonXpath) async {\n    logEventController\n        .add('[Captcha WebView] Filling input and clicking button');\n    final escapedCode =\n        captchaCode.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedInput =\n        inputXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedButton =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final script = '''\n(function() {\n  function evalXpath(xpath) {\n    try {\n      var r = document.evaluate(xpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return r.singleNodeValue;\n    } catch(e) { return null; }\n  }\n  var inputEl = evalXpath('$escapedInput');\n  if (inputEl) {\n    inputEl.focus();\n    var nativeInput = Object.getOwnPropertyDescriptor(\n      window.HTMLInputElement.prototype, 'value');\n    nativeInput.set.call(inputEl, '$escapedCode');\n    inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n    inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n    try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Input filled'); } catch(e) {}\n  } else {\n    try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Input element not found'); } catch(e) {}\n  }\n  var btnEl = evalXpath('$escapedButton');\n  if (btnEl) {\n    btnEl.click();\n    try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Button clicked'); } catch(e) {}\n  } else {\n    try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Button element not found'); } catch(e) {}\n  }\n})();\n''';\n    await webviewController?.evaluateJavascript(source: script);\n  }\n\n  @override\n  Future<String> getCookieString(String pageUrl) async {\n    try {\n      final PlatformCookieManager cookieManager = PlatformCookieManager(\n        PlatformCookieManagerCreationParams(),\n      );\n      final cookies = await cookieManager.getCookies(url: WebUri(pageUrl));\n      return cookies.map((c) => '${c.name}=${c.value}').join('; ');\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] getCookieString error: $e');\n      return '';\n    }\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    try {\n      await webviewController\n          ?.loadUrl(urlRequest: URLRequest(url: WebUri('about:blank')));\n    } catch (_) {}\n  }\n\n  @override\n  void dispose() {\n    _currentCaptchaImageXpath = '';\n    _currentInputXpath = '';\n    captchaWasFound = false;\n    buttonWasClicked = false;\n    _handlersRegistered = false;\n    try {\n      PlatformCookieManager(const PlatformCookieManagerCreationParams())\n          .deleteAllCookies();\n    } catch (_) {}\n    try {\n      captchaImageFoundController.close();\n      captchaDisappearedController.close();\n      initEventController.close();\n      logEventController.close();\n    } catch (_) {}\n    _headlessWebView?.dispose();\n    _headlessWebView = null;\n    webviewController = null;\n  }\n}\n"
  },
  {
    "path": "lib/webview/captcha/impl/captcha_webview_linux_impl.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:desktop_webview_window/desktop_webview_window.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/webview/captcha/captcha_webview_controller.dart';\n\nclass CaptchaWebviewLinuxImpl\n    extends CaptchaWebviewController<Webview> {\n  VoidCallback? _navigationListener;\n  String _currentCaptchaImageXpath = '';\n  String _buttonXpath = '';\n\n  @override\n  Future<void> init() async {\n    final proxyConfig = _getProxyConfiguration();\n    webviewController ??= await WebviewWindow.create(\n      configuration: CreateConfiguration(\n        headless: true,\n        proxy: proxyConfig,\n      ),\n    );\n    _initMessageBridge();\n    _initNavigationListener();\n    initEventController.add(true);\n  }\n\n  ProxyConfiguration? _getProxyConfiguration() {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) return null;\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final parsed = ProxyUtils.parseProxyUrl(proxyUrl);\n    if (parsed == null) return null;\n\n    final (host, port) = parsed;\n    KazumiLogger().i('[Captcha WebView] 代理设置成功 $host:$port');\n    return ProxyConfiguration(host: host, port: port);\n  }\n\n  void _initMessageBridge() {\n    webviewController?.addOnWebMessageReceivedCallback((message) async {\n      final msg = message.toString();\n      logEventController.add('[Captcha WebView] WM: $msg');\n      if (msg.startsWith('captchaImage:')) {\n        final src = msg.replaceFirst('captchaImage:', '');\n        if (src.isNotEmpty && !captchaImageFoundController.isClosed) {\n          captchaWasFound = true;\n          captchaImageFoundController.add(src);\n        }\n      } else if (msg.startsWith('buttonClicked:')) {\n        buttonWasClicked = true;\n        logEventController.add('[Captcha WebView] Button clicked flag set');\n      } else if (msg.startsWith('captchaGone:')) {\n        buttonWasClicked = false;\n        if (!captchaDisappearedController.isClosed) {\n          captchaDisappearedController.add(null);\n        }\n      } else if (msg.startsWith('captchaLog:')) {\n        logEventController.add(\n            '[Captcha WebView JS] ${msg.replaceFirst('captchaLog:', '')}');\n      }\n    });\n  }\n\n  void _initNavigationListener() {\n    _navigationListener = () {\n      _onNavigationInject();\n      _onNavigationCompletion();\n    };\n    webviewController?.isNavigating.addListener(_navigationListener!);\n  }\n\n  Future<void> _onNavigationInject() async {\n    if (webviewController?.isNavigating.value == false) {\n      logEventController.add('[Captcha WebView] Navigation completed');\n      if (_currentCaptchaImageXpath.isNotEmpty) {\n        await _injectCaptchaScript();\n      } else if (_buttonXpath.isNotEmpty) {\n        await _injectButtonClickScript(_buttonXpath);\n      }\n    }\n  }\n\n  Future<void> _onNavigationCompletion() async {\n    if (webviewController?.isNavigating.value == false) {\n      // Type-1: captcha image was seen; check if it has disappeared.\n      if (captchaWasFound) {\n        final present = await _isCaptchaPresent();\n        if (!present && !captchaDisappearedController.isClosed) {\n          logEventController\n              .add('[Captcha WebView] Captcha gone after navigation');\n          captchaWasFound = false;\n          captchaDisappearedController.add(null);\n        }\n      }\n      // Type-2: button was clicked; page navigation confirms verification.\n      if (buttonWasClicked && !captchaDisappearedController.isClosed) {\n        logEventController.add(\n            '[Captcha WebView] Button click and page navigated, verification done');\n        buttonWasClicked = false;\n        captchaDisappearedController.add(null);\n      }\n    }\n  }\n\n  Future<bool> _isCaptchaPresent() async {\n    if (_currentCaptchaImageXpath.isEmpty || webviewController == null) return false;\n    final escaped =\n        _currentCaptchaImageXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    try {\n      final result = await webviewController!.evaluateJavaScript('''\n(function() {\n  try {\n    var r = document.evaluate('$escaped', document, null,\n      XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n    return r.singleNodeValue ? 'present' : 'absent';\n  } catch(e) { return 'absent'; }\n})();\n''');\n      return result?.contains('present') ?? false;\n    } catch (e) {\n      KazumiLogger().d('[Captcha WebView] _isCaptchaPresent error: $e');\n      return false;\n    }\n  }\n\n  Future<void> _injectCaptchaScript() async {\n    if (_currentCaptchaImageXpath.isEmpty) return;\n    final escapedXpath =\n        _currentCaptchaImageXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n\n    final script = '''\n(function() {\n  window.webkit.messageHandlers.msgToNative.postMessage(\n    'captchaLog:CaptchaScript injected on ' + window.location.href);\n\n  var _captchaXpath = '$escapedXpath';\n  var _captchaPoller = null;\n  var _disappearObserver = null;\n\n  function _resolveSrc(node) {\n    return node.getAttribute('src') || node.getAttribute('data-src') ||\n           node.src || '';\n  }\n\n  function _evalXpath() {\n    try {\n      var result = document.evaluate(\n        _captchaXpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return result.singleNodeValue;\n    } catch(e) { return null; }\n  }\n\n  function _startDisappearMonitor() {\n    if (_disappearObserver) return;\n    _disappearObserver = new MutationObserver(function() {\n      if (!_evalXpath()) {\n        _disappearObserver.disconnect();\n        _disappearObserver = null;\n        window.webkit.messageHandlers.msgToNative.postMessage('captchaGone:');\n      }\n    });\n    _disappearObserver.observe(document.documentElement,\n      { childList: true, subtree: true, attributes: true });\n  }\n\n  function _captureAsBase64(imgNode, callback) {\n    function doCapture() {\n      try {\n        var canvas = document.createElement('canvas');\n        canvas.width = imgNode.naturalWidth || imgNode.width || 100;\n        canvas.height = imgNode.naturalHeight || imgNode.height || 40;\n        var ctx = canvas.getContext('2d');\n        ctx.drawImage(imgNode, 0, 0);\n        callback(canvas.toDataURL('image/png'));\n      } catch(e) { callback(null); }\n    }\n    if (imgNode.complete && imgNode.naturalWidth > 0) {\n      doCapture();\n    } else {\n      imgNode.addEventListener('load', doCapture);\n      imgNode.addEventListener('error', function() { callback(null); });\n    }\n  }\n\n  function _checkForCaptcha() {\n    var node = _evalXpath();\n    if (node) {\n      _captureAsBase64(node, function(dataUrl) {\n        if (dataUrl) {\n          window.webkit.messageHandlers.msgToNative.postMessage('captchaImage:' + dataUrl);\n        }\n      });\n      _startDisappearMonitor();\n      return true;\n    }\n    return false;\n  }\n\n  if (!_checkForCaptcha()) {\n    _captchaPoller = setInterval(function() {\n      if (_checkForCaptcha()) {\n        clearInterval(_captchaPoller);\n        _captchaPoller = null;\n      }\n    }, 500);\n  }\n})();\n''';\n\n    try {\n      await webviewController?.evaluateJavaScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] inject script error: $e');\n    }\n  }\n\n  @override\n  Future<void> loadPage(String url, String captchaXpath, {String? inputXpath}) async {\n    _currentCaptchaImageXpath = captchaXpath;\n    _buttonXpath = '';\n    buttonWasClicked = false;\n    captchaWasFound = false;\n    webviewController?.launch(url);\n  }\n\n  @override\n  Future<void> loadPageForButtonClick(String url, String buttonXpath) async {\n    _currentCaptchaImageXpath = '';\n    _buttonXpath = buttonXpath;\n    buttonWasClicked = false;\n    captchaWasFound = false;\n    webviewController?.launch(url);\n  }\n\n  Future<void> _injectButtonClickScript(String buttonXpath) async {\n    final escaped =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final script = '''\n(function() {\n  window.webkit.messageHandlers.msgToNative.postMessage(\n    'captchaLog:ButtonClickScript injected on ' + window.location.href);\n\n  var _xpath = '$escaped';\n  var _clicked = false;\n  var _poller = null;\n  var _disappearObserver = null;\n\n  function evalXpath() {\n    try {\n      var r = document.evaluate(_xpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return r.singleNodeValue;\n    } catch(e) { return null; }\n  }\n\n  function startDisappearMonitor() {\n    if (_disappearObserver) return;\n    _disappearObserver = new MutationObserver(function() {\n      if (!evalXpath()) {\n        _disappearObserver.disconnect();\n        _disappearObserver = null;\n        window.webkit.messageHandlers.msgToNative.postMessage('captchaGone:');\n      }\n    });\n    _disappearObserver.observe(document.documentElement,\n      { childList: true, subtree: true, attributes: true });\n  }\n\n  function checkAndClick() {\n    var btn = evalXpath();\n    if (btn && !_clicked) {\n      _clicked = true;\n      btn.click();\n      window.webkit.messageHandlers.msgToNative.postMessage('buttonClicked:');\n      startDisappearMonitor();\n      return true;\n    }\n    return false;\n  }\n\n  if (!checkAndClick()) {\n    _poller = setInterval(function() {\n      if (checkAndClick()) { clearInterval(_poller); _poller = null; }\n    }, 500);\n  }\n})();\n''';\n    try {\n      await webviewController?.evaluateJavaScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] injectButtonClickScript error: $e');\n    }\n  }\n\n  @override\n  Future<void> submitCaptchaInteract(\n      String captchaCode, String inputXpath, String buttonXpath) async {\n    logEventController\n        .add('[Captcha WebView] Filling input and clicking button');\n    final escapedCode =\n        captchaCode.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedInput =\n        inputXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedButton =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final script = '''\n(function() {\n  function evalXpath(xpath) {\n    try {\n      var r = document.evaluate(xpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return r.singleNodeValue;\n    } catch(e) { return null; }\n  }\n  var inputEl = evalXpath('$escapedInput');\n  if (inputEl) {\n    inputEl.focus();\n    var nativeInput = Object.getOwnPropertyDescriptor(\n      window.HTMLInputElement.prototype, 'value');\n    nativeInput.set.call(inputEl, '$escapedCode');\n    inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n    inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n    window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Input filled');\n  } else {\n    window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Input element not found');\n  }\n  var btnEl = evalXpath('$escapedButton');\n  if (btnEl) {\n    btnEl.click();\n    window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Button clicked');\n  } else {\n    window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Button element not found');\n  }\n})();\n''';\n    try {\n      await webviewController?.evaluateJavaScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] submitCaptchaInteract error: $e');\n    }\n  }\n\n  @override\n  Future<String> getCookieString(String pageUrl) async {\n    try {\n      final cookies = await webviewController?.getAllCookies() ?? [];\n      final cookieString =\n          cookies.map((c) => '${c.name}=${c.value}').join('; ');\n      logEventController\n          .add('[Captcha WebView] Cookies: $cookieString');\n      return cookieString;\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] getCookieString error: $e');\n      return '';\n    }\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    webviewController?.launch('about:blank');\n  }\n\n  @override\n  void dispose() {\n    _currentCaptchaImageXpath = '';\n    _buttonXpath = '';\n    buttonWasClicked = false;\n    captchaWasFound = false;\n    if (_navigationListener != null) {\n      try {\n        webviewController?.isNavigating.removeListener(_navigationListener!);\n      } catch (_) {}\n      _navigationListener = null;\n    }\n    try {\n      captchaImageFoundController.close();\n      captchaDisappearedController.close();\n      initEventController.close();\n      logEventController.close();\n    } catch (_) {}\n    webviewController?.close();\n    webviewController = null;\n  }\n}\n"
  },
  {
    "path": "lib/webview/captcha/impl/captcha_webview_windows_impl.dart",
    "content": "import 'dart:async';\n\nimport 'package:webview_windows/webview_windows.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/webview/captcha/captcha_webview_controller.dart';\n\nclass CaptchaWebviewWindowsImpl\n    extends CaptchaWebviewController<WebviewController> {\n  HeadlessWebview? _headlessWebview;\n  final List<StreamSubscription> _subscriptions = [];\n  String _currentCaptchaImageXpath = '';\n  String _currentInputXpath = '';\n  String _currentPageUrl = '';\n  String _buttonXpath = '';\n\n  @override\n  Future<void> init() async {\n    await _setupProxy();\n    _headlessWebview ??= HeadlessWebview();\n    await _headlessWebview!.run();\n    await _headlessWebview!.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny);\n\n    // Listen for messages from JavaScript via window.chrome.webview.postMessage\n    _subscriptions.add(\n      _headlessWebview!.webMessage.listen(_onWebMessage),\n    );\n\n    // Inject captcha or button-click script when navigation completes\n    _subscriptions.add(\n      _headlessWebview!.loadingState.listen((state) async {\n        if (state == LoadingState.navigationCompleted) {\n          logEventController\n              .add('[Captcha WebView] Navigation completed: $_currentPageUrl');\n          if (_currentCaptchaImageXpath.isNotEmpty) {\n            await _injectCaptchaScript();\n          } else if (_buttonXpath.isNotEmpty) {\n            await _injectButtonClickScript(_buttonXpath);\n          }\n        }\n      }),\n    );\n\n    // After a navigation, detect verification completion for both type-1\n    // (captcha image gone) and type-2 (button was clicked, page navigated).\n    _subscriptions.add(\n      _headlessWebview!.loadingState.listen((state) async {\n        if (state == LoadingState.navigationCompleted) {\n          if (captchaWasFound) {\n            final present = await _isCaptchaPresent();\n            if (!present && !captchaDisappearedController.isClosed) {\n              logEventController\n                  .add('[Captcha WebView] Captcha gone after navigation');\n              captchaWasFound = false;\n              captchaDisappearedController.add(null);\n            }\n          }\n          if (buttonWasClicked && !captchaDisappearedController.isClosed) {\n            logEventController\n                .add('[Captcha WebView] Button click → page navigated, verification done');\n            buttonWasClicked = false;\n            captchaDisappearedController.add(null);\n          }\n        }\n      }),\n    );\n\n    initEventController.add(true);\n  }\n\n  void _onWebMessage(dynamic message) {\n    final msg = message.toString();\n    logEventController.add('[Captcha WebView] WM: $msg');\n    if (msg.startsWith('captchaImage:')) {\n      final src = msg.replaceFirst('captchaImage:', '');\n      if (src.isNotEmpty && !captchaImageFoundController.isClosed) {\n        captchaWasFound = true;\n        captchaImageFoundController.add(src);\n      }\n    } else if (msg.startsWith('buttonClicked:')) {\n      buttonWasClicked = true;\n      logEventController.add('[Captcha WebView] Button clicked flag set');\n    } else if (msg.startsWith('captchaGone:')) {\n      buttonWasClicked = false;\n      if (!captchaDisappearedController.isClosed) {\n        captchaDisappearedController.add(null);\n      }\n    } else if (msg.startsWith('captchaLog:')) {\n      logEventController.add('[Captcha WebView JS] ${msg.replaceFirst('captchaLog:', '')}');\n    }\n  }\n\n  Future<bool> _isCaptchaPresent() async {\n    if (_currentCaptchaImageXpath.isEmpty || _headlessWebview == null) return false;\n    final escaped =\n        _currentCaptchaImageXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    try {\n      final result = await _headlessWebview!.executeScript('''\n(function() {\n  try {\n    var r = document.evaluate('$escaped', document, null,\n      XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n    return r.singleNodeValue ? 'present' : 'absent';\n  } catch(e) { return 'absent'; }\n})();\n''');\n      return result?.toString().contains('present') ?? false;\n    } catch (e) {\n      KazumiLogger().d('[Captcha WebView] _isCaptchaPresent error: $e');\n      return false;\n    }\n  }\n\n  Future<void> _injectCaptchaScript() async {\n    if (_currentCaptchaImageXpath.isEmpty) return;\n    final escapedXpath =\n        _currentCaptchaImageXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedInputXpath =\n        _currentInputXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n\n    final script = '''\n(function() {\n  window.chrome.webview.postMessage('captchaLog:CaptchaScript injected on ' + window.location.href);\n\n  var _captchaXpath = '$escapedXpath';\n  var _inputXpath = '$escapedInputXpath';\n  var _captchaPoller = null;\n  var _disappearObserver = null;\n\n  function _resolveSrc(node) {\n    return node.getAttribute('src') || node.getAttribute('data-src') ||\n           node.src || '';\n  }\n\n  function _evalXpath() {\n    try {\n      var result = document.evaluate(\n        _captchaXpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return result.singleNodeValue;\n    } catch(e) { return null; }\n  }\n\n  function _startDisappearMonitor() {\n    if (_disappearObserver) return;\n    _disappearObserver = new MutationObserver(function() {\n      if (!_evalXpath()) {\n        _disappearObserver.disconnect();\n        _disappearObserver = null;\n        window.chrome.webview.postMessage('captchaGone:');\n      }\n    });\n    _disappearObserver.observe(document.documentElement,\n      { childList: true, subtree: true, attributes: true });\n  }\n\n  function _captureAsBase64(imgNode, callback) {\n    function doCapture() {\n      try {\n        var canvas = document.createElement('canvas');\n        canvas.width = imgNode.naturalWidth || imgNode.width || 100;\n        canvas.height = imgNode.naturalHeight || imgNode.height || 40;\n        var ctx = canvas.getContext('2d');\n        ctx.drawImage(imgNode, 0, 0);\n        callback(canvas.toDataURL('image/png'));\n      } catch(e) { callback(null); }\n    }\n    if (imgNode.complete && imgNode.naturalWidth > 0) {\n      doCapture();\n    } else {\n      imgNode.addEventListener('load', doCapture);\n      imgNode.addEventListener('error', function() { callback(null); });\n    }\n  }\n\n  function _checkForCaptcha() {\n    var node = _evalXpath();\n    if (node) {\n      _captureAsBase64(node, function(dataUrl) {\n        if (dataUrl) {\n          window.chrome.webview.postMessage('captchaImage:' + dataUrl);\n        }\n      });\n      _startDisappearMonitor();\n      return true;\n    }\n    return false;\n  }\n\n  function _triggerInputFocus() {\n    if (!_inputXpath) {\n      return false;\n    }\n    \n    try {\n      var inputResult = document.evaluate(_inputXpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      var inputEl = inputResult.singleNodeValue;\n      \n      if (inputEl) {\n        if (typeof \\$ !== 'undefined' && \\$) {\n          \\$(inputEl).trigger('focus');\n          return true;\n        } else if (typeof jQuery !== 'undefined' && jQuery) {\n          jQuery(inputEl).trigger('focus');\n          return true;\n        } else {\n          inputEl.focus();\n          return true;\n        }\n      }\n    } catch(e) {\n      window.chrome.webview.postMessage('captchaLog:Failed to trigger input focus - ' + e.message);\n    }\n    return false;\n  }\n\n  // If inputXpath is provided, trigger focus to load captcha (some sites require this)\n  _triggerInputFocus();\n  \n  if (!_checkForCaptcha()) {\n    _captchaPoller = setInterval(function() {\n      if (_checkForCaptcha()) {\n        clearInterval(_captchaPoller);\n        _captchaPoller = null;\n      }\n    }, 500);\n  }\n})();\n''';\n\n    try {\n      await _headlessWebview?.executeScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] inject script error: $e');\n    }\n  }\n\n  @override\n  Future<void> loadPage(String url, String captchaXpath, {String? inputXpath}) async {\n    _currentCaptchaImageXpath = captchaXpath;\n    _currentInputXpath = inputXpath ?? '';\n    _buttonXpath = '';\n    buttonWasClicked = false;\n    _currentPageUrl = url;\n    captchaWasFound = false;\n    await _headlessWebview?.loadUrl(url);\n  }\n\n  @override\n  Future<void> loadPageForButtonClick(String url, String buttonXpath) async {\n    _currentCaptchaImageXpath = '';\n    _buttonXpath = buttonXpath;\n    buttonWasClicked = false;\n    _currentPageUrl = url;\n    captchaWasFound = false;\n    await _headlessWebview?.loadUrl(url);\n  }\n\n  Future<void> _injectButtonClickScript(String buttonXpath) async {\n    final escaped =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final script = '''\n(function() {\n  window.chrome.webview.postMessage('captchaLog:ButtonClickScript injected on ' + window.location.href);\n\n  var _xpath = '$escaped';\n  var _clicked = false;\n  var _poller = null;\n  var _disappearObserver = null;\n\n  function evalXpath() {\n    try {\n      var r = document.evaluate(_xpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return r.singleNodeValue;\n    } catch(e) { return null; }\n  }\n\n  function startDisappearMonitor() {\n    if (_disappearObserver) return;\n    _disappearObserver = new MutationObserver(function() {\n      if (!evalXpath()) {\n        _disappearObserver.disconnect();\n        _disappearObserver = null;\n        window.chrome.webview.postMessage('captchaGone:');\n      }\n    });\n    _disappearObserver.observe(document.documentElement,\n      { childList: true, subtree: true, attributes: true });\n  }\n\n  function checkAndClick() {\n    var btn = evalXpath();\n    if (btn && !_clicked) {\n      _clicked = true;\n      btn.click();\n      window.chrome.webview.postMessage('buttonClicked:');\n      startDisappearMonitor();\n      return true;\n    }\n    return false;\n  }\n\n  if (!checkAndClick()) {\n    _poller = setInterval(function() {\n      if (checkAndClick()) { clearInterval(_poller); _poller = null; }\n    }, 500);\n  }\n})();\n''';\n    try {\n      await _headlessWebview?.executeScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] injectButtonClickScript error: $e');\n    }\n  }\n\n  @override\n  Future<void> submitCaptchaInteract(\n      String captchaCode, String inputXpath, String buttonXpath) async {\n    logEventController\n        .add('[Captcha WebView] Filling input and clicking button');\n    final escapedCode =\n        captchaCode.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedInput =\n        inputXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final escapedButton =\n        buttonXpath.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\");\n    final script = '''\n(function() {\n  function evalXpath(xpath) {\n    try {\n      var r = document.evaluate(xpath, document, null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE, null);\n      return r.singleNodeValue;\n    } catch(e) { return null; }\n  }\n  var inputEl = evalXpath('$escapedInput');\n  if (inputEl) {\n    inputEl.focus();\n    var nativeInput = Object.getOwnPropertyDescriptor(\n      window.HTMLInputElement.prototype, 'value');\n    nativeInput.set.call(inputEl, '$escapedCode');\n    inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n    inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n    window.chrome.webview.postMessage('captchaLog:Input filled');\n  } else {\n    window.chrome.webview.postMessage('captchaLog:Input element not found');\n  }\n  var btnEl = evalXpath('$escapedButton');\n  if (btnEl) {\n    btnEl.click();\n    window.chrome.webview.postMessage('captchaLog:Button clicked');\n  } else {\n    window.chrome.webview.postMessage('captchaLog:Button element not found');\n  }\n})();\n''';\n    try {\n      await _headlessWebview?.executeScript(script);\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] submitCaptchaInteract error: $e');\n    }\n  }\n\n  @override\n  Future<String> getCookieString(String pageUrl) async {\n    try {\n      final result = await _headlessWebview?.getCookies(pageUrl);\n      return result ?? '';\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] getCookieString error: $e');\n      return '';\n    }\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    try {\n      await _headlessWebview?.executeScript(\n          \"window.location.href = 'about:blank';\");\n    } catch (e) {\n      KazumiLogger().d('[Captcha WebView] unloadPage skipped: $e');\n    }\n  }\n\n  @override\n  void dispose() {\n    _currentCaptchaImageXpath = '';\n    _currentInputXpath = '';\n    _buttonXpath = '';\n    buttonWasClicked = false;\n    _currentPageUrl = '';\n    for (final s in _subscriptions) {\n      try {\n        s.cancel();\n      } catch (_) {}\n    }\n    _subscriptions.clear();\n    try {\n      captchaImageFoundController.close();\n      captchaDisappearedController.close();\n      initEventController.close();\n      logEventController.close();\n    } catch (_) {}\n    _headlessWebview?.dispose();\n    _headlessWebview = null;\n  }\n\n  Future<void> _setupProxy() async {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) return;\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl);\n    if (formattedProxy == null) return;\n\n    try {\n      await WebviewController.initializeEnvironment(\n        additionalArguments: '--proxy-server=$formattedProxy',\n      );\n      KazumiLogger().i('[Captcha WebView] 代理设置成功 $formattedProxy');\n    } catch (e) {\n      KazumiLogger().e('[Captcha WebView] 设置代理失败 $e');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/impl/video_webview_android_impl.dart",
    "content": "import 'dart:async';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart';\nimport 'package:flutter_inappwebview_android/flutter_inappwebview_android.dart'\n    as android_webview;\n\nclass VideoWebviewAndroidImpl\n    extends VideoWebviewController<PlatformInAppWebViewController> {\n  PlatformHeadlessInAppWebView? headlessWebView;\n  bool hasInjectedScripts = false;\n  bool shouldInjectIframeRedirect = false;\n\n  @override\n  Future<void> init() async {\n    await _setupProxy();\n    headlessWebView ??= PlatformHeadlessInAppWebView(\n      PlatformHeadlessInAppWebViewCreationParams(\n        initialSettings: InAppWebViewSettings(\n          userAgent: Utils.getRandomUA(),\n          mediaPlaybackRequiresUserGesture: true,\n          cacheEnabled: false,\n          blockNetworkImage: true,\n          loadsImagesAutomatically: false,\n          upgradeKnownHostsToHTTPS: false,\n          safeBrowsingEnabled: false,\n          mixedContentMode: MixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE,\n          geolocationEnabled: false,\n        ),\n        onWebViewCreated: (controller) {\n          print('[WebView] Created');\n          webviewController = controller;\n          initEventController.add(true);\n        },\n        onLoadStart: (controller, url) async {\n          logEventController.add('started loading: $url');\n        },\n        onLoadStop: (controller, url) {\n          logEventController.add('loading completed: $url');\n        },\n      ),\n    );\n    await headlessWebView?.run();\n  }\n\n  @override\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0}) async {\n    await unloadPage();\n    if (!hasInjectedScripts) {\n      addJavaScriptHandlers(useLegacyParser);\n      await addUserScripts(useLegacyParser);\n      hasInjectedScripts = true;\n    }\n    count = 0;\n    this.offset = offset;\n    isIframeLoaded = false;\n    isVideoSourceLoaded = false;\n    shouldInjectIframeRedirect = true;\n    videoLoadingEventController.add(true);\n\n    await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url)));\n  }\n\n  void addJavaScriptHandlers(bool useLegacyParser) {\n    logEventController.add('Adding LogBridge handler');\n    webviewController?.addJavaScriptHandler(\n        handlerName: 'LogBridge',\n        callback: (args) {\n          String message = args[0].toString();\n          if (message.contains('about:blank')) {\n            return;\n          }\n          logEventController.add(message);\n        });\n\n    if (useLegacyParser) {\n      logEventController.add('Adding JSBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'JSBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            logEventController.add(\n                'If there is audio but no video, please report it to the rule developer.');\n            if ((message.contains('http') || message.startsWith('//')) &&\n                !message.contains('googleads') &&\n                !message.contains('googlesyndication.com') &&\n                !message.contains('prestrain.html') &&\n                !message.contains('prestrain%2Ehtml') &&\n                !message.contains('adtrafficquality')) {\n              logEventController.add('Parsing video source $message');\n              String encodedUrl = Uri.encodeFull(message);\n              if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) {\n                isIframeLoaded = true;\n                isVideoSourceLoaded = true;\n                videoLoadingEventController.add(false);\n                logEventController.add(\n                    'Loading video source ${Utils.decodeVideoSource(encodedUrl)}');\n                unloadPage();\n                videoParserEventController\n                    .add((Utils.decodeVideoSource(encodedUrl), offset));\n              }\n            }\n          });\n    } else {\n      logEventController.add('Adding VideoBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'VideoBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            if (message.contains('http') && !isVideoSourceLoaded) {\n              logEventController.add('Loading video source: $message');\n              isIframeLoaded = true;\n              isVideoSourceLoaded = true;\n              videoLoadingEventController.add(false);\n              unloadPage();\n              videoParserEventController.add((message, offset));\n            }\n          });\n    }\n  }\n\n  Future<void> addUserScripts(bool useLegacyParser) async {\n    final List<UserScript> scripts = [];\n\n    if (useLegacyParser) {\n      logEventController.add('Adding JSBridgeDebug UserScript');\n      const String jsBridgeDebugScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href);\n        function processIframeElement(iframe) {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Processing iframe element');\n          let src = iframe.getAttribute('src');\n          if (src) {\n            window.flutter_inappwebview.callHandler('JSBridgeDebug', src);\n          }\n        }\n\n        const _observer = new MutationObserver((mutations) => {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for iframes...');\n          mutations.forEach(mutation => {\n            if (mutation.type === 'attributes' && mutation.target.nodeName === 'IFRAME') {\n              processIframeElement(mutation.target);\n            } else {\n              mutation.addedNodes.forEach(node => {\n                if (node.nodeName === 'IFRAME') processIframeElement(node);\n                if (node.querySelectorAll) {\n                  node.querySelectorAll('iframe').forEach(processIframeElement);\n                }\n              });\n            }\n          });  \n        });\n\n        _observer.observe(document.documentElement, {\n          childList: true,\n          subtree: true,\n          attributes: true,\n          attributeFilter: ['src']\n        });\n      \"\"\";\n      scripts.add(UserScript(\n        source: jsBridgeDebugScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,\n      ));\n    } else {\n      logEventController.add('Adding VideoBridgeDebug UserScripts');\n      const String blobParserScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href);\n        const _r_text = window.Response.prototype.text;\n        window.Response.prototype.text = function () {\n            return new Promise((resolve, reject) => {\n                _r_text.call(this).then((text) => {\n                    resolve(text);\n                    if (text.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url);\n                    }\n                }).catch(reject);\n            });\n        }\n\n        const _open = window.XMLHttpRequest.prototype.open;\n        window.XMLHttpRequest.prototype.open = function (...args) {\n            this.addEventListener(\"load\", () => {\n                try {\n                    let content = this.responseText;\n                    if (content.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]);\n                    };\n                } catch {}\n            });\n            return _open.apply(this, args);\n        };\n      \"\"\";\n\n      const String videoTagParserScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href);\n        const _observer = new MutationObserver((mutations) => {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...');\n          for (const mutation of mutations) {\n            if (mutation.type === \"attributes\" && mutation.target.nodeName === \"VIDEO\") {\n              if (processVideoElement(mutation.target)) return;\n              continue;\n            }\n            for (const node of mutation.addedNodes) {\n              if (node.nodeName === \"VIDEO\") {\n                if (processVideoElement(node)) return;\n              }\n              if (node.querySelectorAll) {\n                for (const video of node.querySelectorAll(\"video\")) {\n                  if (processVideoElement(video)) return;\n                }\n              }\n            }\n          }\n        });\n        function processVideoElement(video) {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL');\n          let src = video.getAttribute('src');\n          if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n            _observer.disconnect();\n            window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src);\n            window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n            return true;\n          }\n          const sources = video.getElementsByTagName('source');\n          for (let source of sources) {\n            src = source.getAttribute('src');\n            if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n              _observer.disconnect();\n              window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src);\n              window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n              return true;\n            }\n          }\n        }\n\n        function setupVideoProcessing() {\n          for (const video of document.querySelectorAll(\"video\")) {\n            if (processVideoElement(video)) return;\n          }\n          _observer.observe(document.body, {\n            childList: true,\n            subtree: true,\n            attributes: true,\n            attributeFilter: ['src']\n          });\n        }\n        if (document.readyState === 'loading') {\n          document.addEventListener('DOMContentLoaded', setupVideoProcessing);\n        } else {\n          setupVideoProcessing();\n        }\n    \"\"\";\n      scripts.add(UserScript(\n        source: blobParserScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,\n      ));\n      scripts.add(UserScript(\n        source: videoTagParserScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,\n      ));\n    }\n\n    await webviewController?.addUserScripts(\n      userScripts: scripts,\n    );\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    await webviewController!\n        .loadUrl(urlRequest: URLRequest(url: WebUri(\"about:blank\")));\n  }\n\n  @override\n  void dispose() {\n    headlessWebView?.dispose();\n    headlessWebView = null;\n    webviewController = null;\n  }\n\n  Future<void> _setupProxy() async {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) {\n      return;\n    }\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl);\n    if (formattedProxy == null) {\n      return;\n    }\n\n    try {\n      final proxyAvailable =\n          await android_webview.AndroidWebViewFeature.instance()\n              .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE);\n      if (!proxyAvailable) {\n        KazumiLogger().w('WebView: 当前 Android 版本不支持代理');\n        return;\n      }\n\n      final proxyController = android_webview.AndroidProxyController.instance();\n      await proxyController.clearProxyOverride();\n      await proxyController.setProxyOverride(\n        settings: ProxySettings(\n          proxyRules: [\n            ProxyRule(url: formattedProxy),\n          ],\n        ),\n      );\n      KazumiLogger().i('WebView: 代理设置成功 $formattedProxy');\n    } catch (e) {\n      KazumiLogger().e('WebView: 设置代理失败 $e');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/impl/video_webview_apple_impl.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart';\n\nclass VideoWebviewAppleImpl\n    extends VideoWebviewController<PlatformInAppWebViewController> {\n  PlatformHeadlessInAppWebView? headlessWebView;\n  bool hasInjectedScripts = false;\n\n  @override\n  Future<void> init() async {\n    headlessWebView ??= PlatformHeadlessInAppWebView(\n      PlatformHeadlessInAppWebViewCreationParams(\n        initialUserScripts: UnmodifiableListView<UserScript>([\n          UserScript(\n            source: '''\n            function removeLazyLoading() {\n              document.querySelectorAll('iframe[loading=\"lazy\"]').forEach(iframe => {\n                console.log('Removing lazy loading from:', iframe.src);\n                iframe.removeAttribute('loading');\n              });\n            }\n            if (document.readyState === 'loading') {\n              document.addEventListener('DOMContentLoaded', removeLazyLoading);\n            } else {\n              removeLazyLoading();\n            }\n          ''',\n            injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,\n          ),\n        ]),\n        initialSettings: InAppWebViewSettings(\n          userAgent: Utils.getRandomUA(),\n          mediaPlaybackRequiresUserGesture: true,\n          useOnLoadResource: false,\n          cacheEnabled: false,\n          isInspectable: false,\n          contentBlockers: [\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?devtools-detector\\.js\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.SCRIPT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(urlFilter: '.*', resourceType: [\n                ContentBlockerTriggerResourceType.IMAGE,\n              ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?googleads\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.DOCUMENT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?googlesyndication\\.com\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.DOCUMENT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?prestrain\\.html\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.DOCUMENT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?prestrain%2Ehtml\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.DOCUMENT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n            ContentBlocker(\n              trigger: ContentBlockerTrigger(\n                  urlFilter: r\"^https?://.+?adtrafficquality\",\n                  resourceType: [\n                    ContentBlockerTriggerResourceType.DOCUMENT,\n                  ]),\n              action:\n                  ContentBlockerAction(type: ContentBlockerActionType.BLOCK),\n            ),\n          ],\n        ),\n        onWebViewCreated: (controller) {\n          KazumiLogger().i('WebView: created');\n          webviewController = controller;\n          initEventController.add(true);\n        },\n        onLoadStart: (controller, url) {\n          logEventController.add('started loading: $url');\n        },\n        onLoadStop: (controller, url) {\n          logEventController.add('loading completed: $url');\n        },\n        onReceivedError: (controller, request, error) {\n          KazumiLogger().e('WebView: error: ${error.toString()} - Request: ${request.url}');\n        },\n      ),\n    );\n    await headlessWebView?.run();\n  }\n\n  @override\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0}) async {\n    await unloadPage();\n    if (!hasInjectedScripts) {\n      addJavaScriptHandlers(useLegacyParser);\n      await addUserScripts(useLegacyParser);\n      hasInjectedScripts = true;\n    }\n    count = 0;\n    this.offset = offset;\n    isIframeLoaded = false;\n    isVideoSourceLoaded = false;\n    videoLoadingEventController.add(true);\n\n    await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url)));\n  }\n\n  void addJavaScriptHandlers(bool useLegacyParser) {\n    logEventController.add('Adding LogBridge handler');\n    webviewController?.addJavaScriptHandler(\n        handlerName: 'LogBridge',\n        callback: (args) {\n          String message = args[0].toString();\n          if (message.contains('about:blank')) {\n            return;\n          }\n          logEventController.add(message);\n        });\n\n    if (useLegacyParser) {\n      logEventController.add('Adding JSBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'JSBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            logEventController.add(\n                'If there is audio but no video, please report it to the rule developer.');\n            if ((message.contains('http') || message.startsWith('//')) &&\n                !message.contains('googleads') &&\n                !message.contains('googlesyndication.com') &&\n                !message.contains('prestrain.html') &&\n                !message.contains('prestrain%2Ehtml') &&\n                !message.contains('adtrafficquality')) {\n              logEventController.add('Parsing video source $message');\n              String encodedUrl = Uri.encodeFull(message);\n              if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) {\n                isIframeLoaded = true;\n                isVideoSourceLoaded = true;\n                videoLoadingEventController.add(false);\n                logEventController.add(\n                    'Loading video source ${Utils.decodeVideoSource(encodedUrl)}');\n                unloadPage();\n                videoParserEventController\n                    .add((Utils.decodeVideoSource(encodedUrl), offset));\n              }\n            }\n          });\n    } else {\n      logEventController.add('Adding VideoBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'VideoBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            if (message.contains('http') && !isVideoSourceLoaded) {\n              logEventController.add('Loading video source: $message');\n              isIframeLoaded = true;\n              isVideoSourceLoaded = true;\n              videoLoadingEventController.add(false);\n              unloadPage();\n              videoParserEventController.add((message, offset));\n            }\n          });\n    }\n  }\n\n  Future<void> addUserScripts(\n      bool useLegacyParser) async {\n    final List<UserScript> scripts = [];\n\n    if (useLegacyParser) {\n      logEventController.add('Adding JSBridgeDebug UserScript');\n      const String jsBridgeDebugScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href);\n        var iframes = document.getElementsByTagName('iframe');\n        window.flutter_inappwebview.callHandler('LogBridge', 'The number of iframe tags is ' + iframes.length);\n        for (var i = 0; i < iframes.length; i++) {\n            var iframe = iframes[i];\n            var src = iframe.getAttribute('src');\n            if (src) {\n              window.flutter_inappwebview.callHandler('JSBridgeDebug', src);\n            }\n        }\n      \"\"\";\n      scripts.add(UserScript(\n        source: jsBridgeDebugScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,\n        forMainFrameOnly: false,\n      ));\n    } else {\n      logEventController.add('Adding VideoBridgeDebug UserScripts');\n      const String blobParserScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href);\n        const _r_text = window.Response.prototype.text;\n        window.Response.prototype.text = function () {\n            return new Promise((resolve, reject) => {\n                _r_text.call(this).then((text) => {\n                    resolve(text);\n                    if (text.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url);\n                    }\n                }).catch(reject);\n            });\n        }\n\n        const _open = window.XMLHttpRequest.prototype.open;\n        window.XMLHttpRequest.prototype.open = function (...args) {\n            this.addEventListener(\"load\", () => {\n                try {\n                    let content = this.responseText;\n                    if (content.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]);\n                    };\n                } catch {}\n            });\n            return _open.apply(this, args);\n        };\n      \"\"\";\n\n      const String videoTagParserScript = \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href);\n        function processVideoElement(video) {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL');\n          let src = video.getAttribute('src');\n          if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n            window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src);\n            window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n            return;\n          }\n          const sources = video.getElementsByTagName('source');\n          for (let source of sources) {\n            src = source.getAttribute('src');\n            if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n              window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src);\n              window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n              return;\n            }\n          }\n        }\n\n        document.querySelectorAll('video').forEach(processVideoElement);\n\n        const _observer = new MutationObserver((mutations) => {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...');\n          mutations.forEach(mutation => {\n            if (mutation.type === 'attributes' && mutation.target.nodeName === 'VIDEO') {\n              processVideoElement(mutation.target);\n            }\n            mutation.addedNodes.forEach(node => {\n              if (node.nodeName === 'VIDEO') processVideoElement(node);\n              if (node.querySelectorAll) {\n                node.querySelectorAll('video').forEach(processVideoElement);\n              }\n            });\n          });  \n        });\n\n        _observer.observe(document.body, {\n          childList: true,\n          subtree: true,\n          attributes: true,\n          attributeFilter: ['src']\n        });\n    \"\"\";\n      scripts.add(UserScript(\n        source: blobParserScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,\n        forMainFrameOnly: false,\n      ));\n      scripts.add(UserScript(\n        source: videoTagParserScript,\n        injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,\n        forMainFrameOnly: false,\n      ));\n    }\n\n    await webviewController?.addUserScripts(\n      userScripts: scripts,\n    );\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    await webviewController!\n        .loadUrl(urlRequest: URLRequest(url: WebUri(\"about:blank\")));\n  }\n\n  @override\n  void dispose() {\n    headlessWebView?.dispose();\n    headlessWebView = null;\n    webviewController = null;\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/impl/video_webview_impl.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart';\nimport 'package:flutter_inappwebview_android/flutter_inappwebview_android.dart'\n    as android_webview;\n\nclass VideoWebviewImpl\n    extends VideoWebviewController<PlatformInAppWebViewController> {\n  PlatformHeadlessInAppWebView? headlessWebView;\n  bool hasRegisteredHandlers = false;\n  bool useLegacyParser = false;\n  Timer? videoParserTimer;\n\n  @override\n  Future<void> init() async {\n    await _setupProxy();\n    headlessWebView ??= PlatformHeadlessInAppWebView(\n      PlatformHeadlessInAppWebViewCreationParams(\n        initialSize: const Size(360, 640),\n        initialSettings: InAppWebViewSettings(\n          userAgent: Utils.getRandomUA(),\n          mediaPlaybackRequiresUserGesture: true,\n          upgradeKnownHostsToHTTPS: false,\n          mixedContentMode: MixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE,\n        ),\n        onWebViewCreated: (controller) {\n          print('[WebView] Created (legacy fallback)');\n          webviewController = controller;\n          initEventController.add(true);\n        },\n        shouldInterceptRequest: (controller, request) async {\n          if (useLegacyParser || isVideoSourceLoaded) return null;\n          final url = request.url.toString();\n          final lower = url.toLowerCase();\n          if (_isAdUrl(lower)) return null;\n          if (_isM3U8Url(lower) ||\n              _isRangeVideoRequest(lower, request.headers)) {\n            logEventController\n                .add('Native intercepted video URL: $url');\n            isIframeLoaded = true;\n            isVideoSourceLoaded = true;\n            videoLoadingEventController.add(false);\n            unloadPage();\n            videoParserEventController.add((url, offset));\n          }\n          return null;\n        },\n        onLoadStart: (controller, url) async {\n          logEventController.add('started loading: $url');\n          if (url.toString() != 'about:blank') {\n            await _onLoadStart();\n          }\n        },\n        onLoadStop: (controller, url) async {\n          logEventController.add('loading completed: $url');\n          if (url.toString() != 'about:blank') {\n            await _onLoadStop();\n          }\n        },\n        onConsoleMessage: (controller, consoleMessage) {\n          logEventController.add(\n              'Console [${consoleMessage.messageLevel}]: ${consoleMessage.message}');\n        },\n        onReceivedError: (controller, request, error) {\n          logEventController.add(\n              'Error: ${error.description} - ${request.url}');\n        },\n      ),\n    );\n    await headlessWebView?.run();\n  }\n\n  @override\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0}) async {\n    await unloadPage();\n    if (!hasRegisteredHandlers) {\n      _addJavaScriptHandlers(useLegacyParser);\n      hasRegisteredHandlers = true;\n    }\n    count = 0;\n    this.offset = offset;\n    this.useLegacyParser = useLegacyParser;\n    isIframeLoaded = false;\n    isVideoSourceLoaded = false;\n    videoLoadingEventController.add(true);\n\n    await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url)));\n  }\n\n  void _addJavaScriptHandlers(bool useLegacyParser) {\n    logEventController.add('Adding LogBridge handler');\n    webviewController?.addJavaScriptHandler(\n        handlerName: 'LogBridge',\n        callback: (args) {\n          String message = args[0].toString();\n          if (message.contains('about:blank')) {\n            return;\n          }\n          logEventController.add(message);\n        });\n\n    if (useLegacyParser) {\n      logEventController.add('Adding JSBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'JSBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            logEventController.add(\n                'If there is audio but no video, please report it to the rule developer.');\n            if ((message.contains('http') || message.startsWith('//')) &&\n                !message.contains('googleads') &&\n                !message.contains('googlesyndication.com') &&\n                !message.contains('prestrain.html') &&\n                !message.contains('prestrain%2Ehtml') &&\n                !message.contains('adtrafficquality')) {\n              logEventController.add('Parsing video source $message');\n              String encodedUrl = Uri.encodeFull(message);\n              if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) {\n                isIframeLoaded = true;\n                isVideoSourceLoaded = true;\n                videoLoadingEventController.add(false);\n                logEventController.add(\n                    'Loading video source ${Utils.decodeVideoSource(encodedUrl)}');\n                unloadPage();\n                videoParserEventController\n                    .add((Utils.decodeVideoSource(encodedUrl), offset));\n              }\n            }\n          });\n    } else {\n      logEventController.add('Adding VideoBridgeDebug handler');\n      webviewController?.addJavaScriptHandler(\n          handlerName: 'VideoBridgeDebug',\n          callback: (args) {\n            String message = args[0].toString();\n            logEventController.add('Callback received: $message');\n            if (message.contains('http') && !isVideoSourceLoaded) {\n              logEventController.add('Loading video source: $message');\n              isIframeLoaded = true;\n              isVideoSourceLoaded = true;\n              videoLoadingEventController.add(false);\n              unloadPage();\n              videoParserEventController.add((message, offset));\n            }\n          });\n    }\n  }\n\n  Future<void> _onLoadStart() async {\n    if (!useLegacyParser) {\n      logEventController.add('Injecting blob parser script (onLoadStart)');\n      await webviewController?.evaluateJavascript(source: \"\"\"\n        try { window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href); } catch(e) {}\n        const _r_text = window.Response.prototype.text;\n        window.Response.prototype.text = function () {\n            return new Promise((resolve, reject) => {\n                _r_text.call(this).then((text) => {\n                    resolve(text);\n                    if (text.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url);\n                    }\n                }).catch(reject);\n            });\n        }\n\n        const _open = window.XMLHttpRequest.prototype.open;\n        window.XMLHttpRequest.prototype.open = function (...args) {\n            this.addEventListener(\"load\", () => {\n                try {\n                    let content = this.responseText;\n                    if (content.trim().startsWith(\"#EXTM3U\")) {\n                        window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]);\n                        window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]);\n                    };\n                } catch {}\n            });\n            return _open.apply(this, args);\n        };\n\n        function injectIntoIframe(iframe) {\n          try {\n            const iframeWindow = iframe.contentWindow;\n            if (!iframeWindow) return;\n\n            const iframe_r_text = iframeWindow.Response.prototype.text;\n            iframeWindow.Response.prototype.text = function () {\n              return new Promise((resolve, reject) => {\n                iframe_r_text.call(this).then((text) => {\n                  resolve(text);\n                  if (text.trim().startsWith(\"#EXTM3U\")) {\n                    window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found in iframe: ' + this.url);\n                    window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url);\n                  }\n                }).catch(reject);\n              });\n            }\n\n            const iframe_open = iframeWindow.XMLHttpRequest.prototype.open;\n            iframeWindow.XMLHttpRequest.prototype.open = function (...args) {\n              this.addEventListener(\"load\", () => {\n                try {\n                  let content = this.responseText;\n                  if (content.trim().startsWith(\"#EXTM3U\") && args[1] !== null && args[1] !== undefined) {\n                    window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found in iframe: ' + args[1]);\n                    window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]);\n                  };\n                } catch {}\n              });\n              return iframe_open.apply(this, args);\n            }\n          } catch (e) {\n            console.error('iframe inject failed:', e);\n          }\n        }\n\n        function setupIframeListeners() {\n          document.querySelectorAll('iframe').forEach(iframe => {\n            if (iframe.contentDocument) {\n              injectIntoIframe(iframe);\n            }\n            iframe.addEventListener('load', () => injectIntoIframe(iframe));\n          });\n\n          const observer = new MutationObserver(mutations => {\n            mutations.forEach(mutation => {\n              if (mutation.type === 'childList') {\n                mutation.addedNodes.forEach(node => {\n                  if (node.nodeName === 'IFRAME') {\n                    node.addEventListener('load', () => injectIntoIframe(node));\n                  }\n                  if (node.querySelectorAll) {\n                    node.querySelectorAll('iframe').forEach(iframe => {\n                      iframe.addEventListener('load', () => injectIntoIframe(iframe));\n                    });\n                  }\n                });\n              }\n            });\n          });\n\n          if (document.body) {\n            observer.observe(document.body, { childList: true, subtree: true });\n          } else {\n            document.addEventListener('DOMContentLoaded', () => {\n              observer.observe(document.body, { childList: true, subtree: true });\n            });\n          }\n        }\n\n        if (document.readyState === 'loading') {\n          document.addEventListener('DOMContentLoaded', setupIframeListeners);\n        } else {\n          setupIframeListeners();\n        }\n      \"\"\");\n    }\n  }\n\n  Future<void> _onLoadStop() async {\n    if (!useLegacyParser) {\n      logEventController.add('Injecting video tag parser script (onLoadStop)');\n      await webviewController?.evaluateJavascript(source: \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href);\n        const _observer = new MutationObserver((mutations) => {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...');\n          for (const mutation of mutations) {\n            if (mutation.type === \"attributes\" && mutation.target.nodeName === \"VIDEO\") {\n              if (processVideoElement(mutation.target)) return;\n              continue;\n            }\n            for (const node of mutation.addedNodes) {\n              if (node.nodeName === \"VIDEO\") {\n                if (processVideoElement(node)) return;\n              }\n              if (node.querySelectorAll) {\n                for (const video of node.querySelectorAll(\"video\")) {\n                  if (processVideoElement(video)) return;\n                }\n              }\n            }\n          }\n        });\n        function processVideoElement(video) {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL');\n          let src = video.getAttribute('src');\n          if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n            _observer.disconnect();\n            window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src);\n            window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n            return true;\n          }\n          const sources = video.getElementsByTagName('source');\n          for (let source of sources) {\n            src = source.getAttribute('src');\n            if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n              _observer.disconnect();\n              window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src);\n              window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n              return true;\n            }\n          }\n        }\n\n        function setupVideoProcessing() {\n          for (const video of document.querySelectorAll(\"video\")) {\n            if (processVideoElement(video)) return;\n          }\n          _observer.observe(document.body, {\n            childList: true,\n            subtree: true,\n            attributes: true,\n            attributeFilter: ['src']\n          });\n        }\n        if (document.readyState === 'loading') {\n          document.addEventListener('DOMContentLoaded', setupVideoProcessing);\n        } else {\n          setupVideoProcessing();\n        }\n      \"\"\");\n    }\n\n    if (useLegacyParser) {\n      logEventController.add('Injecting JSBridgeDebug script (onLoadStop)');\n      await webviewController?.evaluateJavascript(source: \"\"\"\n        window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href);\n        function processIframeElement(iframe) {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Processing iframe element');\n          let src = iframe.getAttribute('src');\n          if (src) {\n            window.flutter_inappwebview.callHandler('JSBridgeDebug', src);\n          }\n        }\n\n        const _observer = new MutationObserver((mutations) => {\n          window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for iframes...');\n          mutations.forEach(mutation => {\n            if (mutation.type === 'attributes' && mutation.target.nodeName === 'IFRAME') {\n              processIframeElement(mutation.target);\n            } else {\n              mutation.addedNodes.forEach(node => {\n                if (node.nodeName === 'IFRAME') processIframeElement(node);\n                if (node.querySelectorAll) {\n                  node.querySelectorAll('iframe').forEach(processIframeElement);\n                }\n              });\n            }\n          });\n        });\n\n        _observer.observe(document.documentElement, {\n          childList: true,\n          subtree: true,\n          attributes: true,\n          attributeFilter: ['src']\n        });\n      \"\"\");\n    }\n\n    _startVideoParserTimer();\n  }\n\n  void _startVideoParserTimer() {\n    videoParserTimer?.cancel();\n    logEventController.add('Starting video parser timer');\n    videoParserTimer = Timer.periodic(const Duration(seconds: 1), (timer) {\n      if (isVideoSourceLoaded) {\n        timer.cancel();\n        return;\n      }\n      _pollVideoSource();\n    });\n  }\n\n  Future<void> _pollVideoSource() async {\n    if (isVideoSourceLoaded) return;\n\n    if (useLegacyParser) {\n      await webviewController?.evaluateJavascript(source: \"\"\"\n        (function() {\n          var iframes = document.querySelectorAll('iframe');\n          window.flutter_inappwebview.callHandler('LogBridge', 'Timer scan: found ' + iframes.length + ' iframe(s)');\n          for (var i = 0; i < iframes.length; i++) {\n            var src = iframes[i].getAttribute('src');\n            if (src) {\n              window.flutter_inappwebview.callHandler('JSBridgeDebug', src);\n            }\n          }\n        })();\n      \"\"\");\n    } else {\n      await webviewController?.evaluateJavascript(source: \"\"\"\n        (function() {\n          var videos = document.querySelectorAll('video');\n          window.flutter_inappwebview.callHandler('LogBridge', 'Timer scan: found ' + videos.length + ' video element(s)');\n          for (var i = 0; i < videos.length; i++) {\n            var src = videos[i].getAttribute('src');\n            if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n              window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src);\n              window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n              return;\n            }\n            var sources = videos[i].getElementsByTagName('source');\n            for (var j = 0; j < sources.length; j++) {\n              src = sources[j].getAttribute('src');\n              if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n                window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src);\n                window.flutter_inappwebview.callHandler('VideoBridgeDebug', src);\n                return;\n              }\n            }\n          }\n        })();\n      \"\"\");\n    }\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    videoParserTimer?.cancel();\n    videoParserTimer = null;\n    await webviewController\n        ?.loadUrl(urlRequest: URLRequest(url: WebUri(\"about:blank\")));\n  }\n\n  @override\n  void dispose() {\n    videoParserTimer?.cancel();\n    videoParserTimer = null;\n    headlessWebView?.dispose();\n    headlessWebView = null;\n    webviewController = null;\n  }\n\n  bool _isM3U8Url(String lower) {\n    final uri = Uri.tryParse(lower);\n    if (uri == null) return false;\n    return uri.path.endsWith('.m3u8');\n  }\n\n  bool _isRangeVideoRequest(String lower, Map<String, String>? headers) {\n    if (headers == null) return false;\n    final range = headers['Range'] ?? headers['range'];\n    if (range == null || !range.startsWith('bytes=')) return false;\n    if (lower.endsWith('.js') ||\n        lower.endsWith('.css') ||\n        lower.endsWith('.html') ||\n        lower.endsWith('.json') ||\n        lower.endsWith('.png') ||\n        lower.endsWith('.jpg') ||\n        lower.endsWith('.gif') ||\n        lower.endsWith('.svg') ||\n        lower.endsWith('.woff') ||\n        lower.endsWith('.woff2') ||\n        lower.endsWith('.wasm')) {\n      return false;\n    }\n    return true;\n  }\n\n  bool _isAdUrl(String lower) {\n    return lower.contains('googleads') ||\n        lower.contains('googlesyndication') ||\n        lower.contains('adtrafficquality') ||\n        lower.contains('doubleclick');\n  }\n\n  Future<void> _setupProxy() async {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) {\n      return;\n    }\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl);\n    if (formattedProxy == null) {\n      return;\n    }\n\n    try {\n      final proxyAvailable =\n          await android_webview.AndroidWebViewFeature.instance()\n              .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE);\n      if (!proxyAvailable) {\n        KazumiLogger().w('WebView: 当前 Android 版本不支持代理');\n        return;\n      }\n\n      final proxyController = android_webview.AndroidProxyController.instance();\n      await proxyController.clearProxyOverride();\n      await proxyController.setProxyOverride(\n        settings: ProxySettings(\n          proxyRules: [\n            ProxyRule(url: formattedProxy),\n          ],\n        ),\n      );\n      KazumiLogger().i('WebView: 代理设置成功 $formattedProxy');\n    } catch (e) {\n      KazumiLogger().e('WebView: 设置代理失败 $e');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/impl/video_webview_linux_impl.dart",
    "content": "import 'dart:async';\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:kazumi/utils/utils.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\nimport 'package:desktop_webview_window/desktop_webview_window.dart';\n\nclass VideoWebviewLinuxImpl extends VideoWebviewController<Webview> {\n  bool bridgeInited = false;\n\n  @override\n  Future<void> init() async {\n    final proxyConfig = _getProxyConfiguration();\n    webviewController ??= await WebviewWindow.create(\n      configuration: CreateConfiguration(\n        headless: true,\n        proxy: proxyConfig,\n        userScripts: const [\n          UserScript(\n              source: blobScript,\n              injectionTime: UserScriptInjectionTime.documentStart,\n              forAllFrames: true),\n          UserScript(\n              source: iframeScript,\n              injectionTime: UserScriptInjectionTime.documentEnd,\n              forAllFrames: true),\n          UserScript(\n              source: videoScript,\n              injectionTime: UserScriptInjectionTime.documentEnd,\n              forAllFrames: true)\n        ],\n      ),\n    );\n    bridgeInited = false;\n    initEventController.add(true);\n  }\n\n  ProxyConfiguration? _getProxyConfiguration() {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) {\n      return null;\n    }\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final parsed = ProxyUtils.parseProxyUrl(proxyUrl);\n    if (parsed == null) {\n      return null;\n    }\n\n    final (host, port) = parsed;\n    KazumiLogger().i('WebView: 代理设置成功 $host:$port');\n    return ProxyConfiguration(host: host, port: port);\n  }\n\n  Future<void> initBridge(bool useLegacyParser) async {\n    await initJSBridge(useLegacyParser);\n    bridgeInited = true;\n  }\n\n  @override\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0}) async {\n    await unloadPage();\n    if (!bridgeInited) {\n      await initBridge(useLegacyParser);\n    }\n    count = 0;\n    this.offset = offset;\n    isIframeLoaded = false;\n    isVideoSourceLoaded = false;\n    videoLoadingEventController.add(true);\n    webviewController!.launch(url);\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    await redirect2Blank();\n  }\n\n  @override\n  void dispose() {\n    webviewController!.close();\n    bridgeInited = false;\n  }\n\n  Future<void> initJSBridge(bool useLegacyParser) async {\n    webviewController!.addOnWebMessageReceivedCallback((message) async {\n      if (message.contains('iframeMessage:')) {\n        String messageItem =\n            Uri.encodeFull(message.replaceFirst('iframeMessage:', ''));\n        logEventController\n            .add('Callback received: [iframe] ${Uri.decodeFull(messageItem)}');\n        if ((messageItem.contains('http') || messageItem.startsWith('//')) &&\n            !messageItem.contains('googleads') &&\n            !messageItem.contains('googlesyndication.com') &&\n            !messageItem.contains('prestrain.html') &&\n            !messageItem.contains('prestrain%2Ehtml') &&\n            !messageItem.contains('adtrafficquality')) {\n          if (Utils.decodeVideoSource(messageItem) !=\n                  Uri.encodeFull(messageItem) &&\n              useLegacyParser) {\n            logEventController.add('Parsing video source $messageItem');\n            isIframeLoaded = true;\n            isVideoSourceLoaded = true;\n            videoLoadingEventController.add(false);\n            logEventController.add(\n                'Loading video source ${Utils.decodeVideoSource(messageItem)}');\n            unloadPage();\n            videoParserEventController\n                .add((Utils.decodeVideoSource(messageItem), offset));\n          }\n        }\n      }\n      if (message.contains('videoMessage:')) {\n        String messageItem =\n            Uri.encodeFull(message.replaceFirst('videoMessage:', ''));\n        logEventController\n            .add('Callback received: [video] ${Uri.decodeFull(messageItem)}');\n        if (messageItem.contains('http')) {\n          String videoUrl = Uri.decodeFull(messageItem);\n          logEventController.add('Loading video source: $videoUrl');\n          isIframeLoaded = true;\n          isVideoSourceLoaded = true;\n          videoLoadingEventController.add(false);\n          unloadPage();\n          videoParserEventController.add((videoUrl, offset));\n        }\n      }\n    });\n  }\n\n  static const String iframeScript = \"\"\"\n    var iframes = document.getElementsByTagName('iframe');\n    for (var i = 0; i < iframes.length; i++) {\n        var iframe = iframes[i];\n        var src = iframe.getAttribute('src');\n        if (src) {\n          window.webkit.messageHandlers.msgToNative.postMessage('iframeMessage:' + src);\n        }\n    }\n  \"\"\";\n\n  static const String videoScript = \"\"\"\n    function processVideoElement(video) {\n      let src = video.getAttribute('src');\n      if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n        window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + src);\n        return;\n      }\n      const sources = video.getElementsByTagName('source');\n      for (let source of sources) {\n        src = source.getAttribute('src');\n        if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) {\n          window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + src);\n          return;\n        }\n      }\n    }\n\n    document.querySelectorAll('video').forEach(processVideoElement);\n\n    const _observer = new MutationObserver((mutations) => {\n      mutations.forEach(mutation => {\n        if (mutation.type === 'attributes' && mutation.target.nodeName === 'VIDEO') {\n          processVideoElement(mutation.target);\n        }\n        mutation.addedNodes.forEach(node => {\n          if (node.nodeName === 'VIDEO') processVideoElement(node);\n          if (node.querySelectorAll) {\n            node.querySelectorAll('video').forEach(processVideoElement);\n          }\n        });\n      });  \n    });\n\n    _observer.observe(document.body, {\n      childList: true,\n      subtree: true,\n      attributes: true,\n      attributeFilter: ['src']\n    });\n  \"\"\";\n\n  static const String blobScript = \"\"\"\n    const _r_text = window.Response.prototype.text;\n    window.Response.prototype.text = function () {\n        return new Promise((resolve, reject) => {\n            _r_text.call(this).then((text) => {\n                resolve(text);\n                if (text.trim().startsWith(\"#EXTM3U\")) {\n                    window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + this.url);\n                }\n            }).catch(reject);\n        });\n    }\n\n    const _open = window.XMLHttpRequest.prototype.open;\n    window.XMLHttpRequest.prototype.open = function (...args) {\n        this.addEventListener(\"load\", () => {\n            try {\n                let content = this.responseText;\n                if (content.trim().startsWith(\"#EXTM3U\")) {\n                    window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + args[1]);\n                };\n            } catch { }\n        });\n        return _open.apply(this, args);\n    }\n  \"\"\";\n\n  Future<void> redirect2Blank() async {\n    webviewController?.launch(\"about:blank\");\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/impl/video_webview_windows_impl.dart",
    "content": "import 'dart:async';\nimport 'package:webview_windows/webview_windows.dart';\nimport 'package:kazumi/webview/video/video_webview_controller.dart';\nimport 'package:kazumi/utils/storage.dart';\nimport 'package:kazumi/utils/proxy_utils.dart';\nimport 'package:kazumi/utils/logger.dart';\n\nclass VideoWebviewWindowsImpl\n    extends VideoWebviewController<WebviewController> {\n  final List<StreamSubscription> subscriptions = [];\n\n  HeadlessWebview? headlessWebview;\n\n  @override\n  Future<void> init() async {\n    await _setupProxy();\n    headlessWebview ??= HeadlessWebview();\n    await headlessWebview!.run();\n    await headlessWebview!.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny);\n    initEventController.add(true);\n  }\n\n  Future<void> _setupProxy() async {\n    final setting = GStorage.setting;\n    final bool proxyEnable =\n        setting.get(SettingBoxKey.proxyEnable, defaultValue: false);\n    if (!proxyEnable) {\n      return;\n    }\n\n    final String proxyUrl =\n        setting.get(SettingBoxKey.proxyUrl, defaultValue: '');\n    final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl);\n    if (formattedProxy == null) {\n      return;\n    }\n\n    try {\n      await WebviewController.initializeEnvironment(\n        additionalArguments: '--proxy-server=$formattedProxy',\n      );\n      KazumiLogger().i('WebView: 代理设置成功 $formattedProxy');\n    } catch (e) {\n      KazumiLogger().e('WebView: 设置代理失败 $e');\n    }\n  }\n\n  @override\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0}) async {\n    await unloadPage();\n    count = 0;\n    this.offset = offset;\n    isIframeLoaded = false;\n    isVideoSourceLoaded = false;\n    videoLoadingEventController.add(true);\n    subscriptions.add(headlessWebview!.onM3USourceLoaded.listen((data) {\n      if (headlessWebview == null) return;\n      String url = data['url'] ?? '';\n      if (url.isEmpty) {\n        return;\n      }\n      unloadPage();\n      isIframeLoaded = true;\n      isVideoSourceLoaded = true;\n      videoLoadingEventController.add(false);\n      logEventController.add('Loading m3u8 source: $url');\n      videoParserEventController.add((url, offset));\n    }));\n    subscriptions.add(headlessWebview!.onVideoSourceLoaded.listen((data) {\n      if (headlessWebview == null) return;\n      String url = data['url'] ?? '';\n      if (url.isEmpty) {\n        return;\n      }\n      unloadPage();\n      isIframeLoaded = true;\n      isVideoSourceLoaded = true;\n      videoLoadingEventController.add(false);\n      logEventController.add('Loading video source: $url');\n      videoParserEventController.add((url, offset));\n    }));\n    await headlessWebview!.loadUrl(url);\n  }\n\n  @override\n  Future<void> unloadPage() async {\n    subscriptions.forEach((s) {\n      try {\n        s.cancel();\n      } catch (_) {}\n    });\n    subscriptions.clear();\n    await redirect2Blank();\n  }\n\n  @override\n  void dispose() {\n    subscriptions.forEach((s) {\n      try {\n        s.cancel();\n      } catch (_) {}\n    });\n    subscriptions.clear();\n    headlessWebview?.dispose();\n    headlessWebview = null;\n  }\n\n  // The webview_windows package does not have a method to unload the current page.\n  // The loadUrl method opens a new tab, which can lead to memory leaks.\n  // Directly disposing of the webview controller would require reinitialization when switching episodes, which is costly.\n  // Therefore, this method is used to redirect to a blank page instead.\n  Future<void> redirect2Blank() async {\n    if (headlessWebview == null) return;\n    try {\n      await headlessWebview!.executeScript('''\n        window.location.href = 'about:blank';\n      ''');\n    } catch (e) {\n      KazumiLogger().d('WebView: redirect2Blank skipped (likely disposed): $e');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/webview/video/video_webview_controller.dart",
    "content": "import 'dart:io';\nimport 'dart:async';\n\nimport 'package:kazumi/webview/video/impl/video_webview_android_impl.dart';\nimport 'package:kazumi/webview/video/impl/video_webview_impl.dart';\nimport 'package:kazumi/webview/video/impl/video_webview_windows_impl.dart';\nimport 'package:kazumi/webview/video/impl/video_webview_linux_impl.dart';\nimport 'package:kazumi/webview/video/impl/video_webview_apple_impl.dart';\nimport 'package:kazumi/utils/utils.dart';\n\nabstract class VideoWebviewController<T> {\n  // Webview controller\n  T? webviewController;\n\n  // Retry count\n  int count = 0;\n  // Last watched position\n  int offset = 0;\n  bool isIframeLoaded = false;\n  bool isVideoSourceLoaded = false;\n\n  /// Webview initialization method\n  Future<void> init();\n\n  final StreamController<bool> initEventController =\n      StreamController<bool>.broadcast();\n\n  // Stream to notify when the webview is initialized\n  Stream<bool> get onInitialized => initEventController.stream;\n\n  final StreamController<String> logEventController =\n      StreamController<String>.broadcast();\n\n  // Stream to subscribe to webview logs\n  Stream<String> get onLog => logEventController.stream;\n\n  final StreamController<bool> videoLoadingEventController =\n      StreamController<bool>.broadcast();\n\n  // Stream to notify when the video source is loaded\n  Stream<bool> get onVideoLoading => videoLoadingEventController.stream;\n\n  // Stream to notify video source URL when the video source is loaded\n  // The first parameter is the video source URL and the second parameter is the video offset (start position)\n  final StreamController<(String, int)> videoParserEventController =\n      StreamController<(String, int)>.broadcast();\n\n  Stream<(String, int)> get onVideoURLParser => videoParserEventController.stream;\n\n  /// Webview load URL method\n  Future<void> loadUrl(String url, bool useLegacyParser,\n      {int offset = 0});\n\n  /// Webview unload page method\n  Future<void> unloadPage();\n\n  /// Webview dispose method\n  void dispose();\n}\n\nclass VideoWebviewControllerFactory {\n  static VideoWebviewController getController() {\n    if (Platform.isWindows) {\n      return VideoWebviewWindowsImpl();\n    }\n    if (Platform.isLinux) {\n      return VideoWebviewLinuxImpl();\n    }\n    if (Platform.isMacOS || Platform.isIOS) {\n      return VideoWebviewAppleImpl();\n    }\n    if (Platform.isAndroid && Utils.isDocumentStartScriptSupported) {\n      return VideoWebviewAndroidImpl();\n    }\n    return VideoWebviewImpl();\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"kazumi\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"io.github.Predidit.Kazumi\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# add runpath, shared libs of a release bundle is in lib dir, plugin must add $ORIGIN to runpath to find libmpv\n# set(CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_SHARED_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\\\$ORIGIN\")\n# set(CMAKE_MODULE_LINKER_FLAGS \"${CMAKE_MODULE_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\\\$ORIGIN\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/linux/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <desktop_webview_window/desktop_webview_window_plugin.h>\n#include <dynamic_color/dynamic_color_plugin.h>\n#include <flutter_volume_controller/flutter_volume_controller_plugin.h>\n#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>\n#include <media_kit_video/media_kit_video_plugin.h>\n#include <screen_retriever_linux/screen_retriever_linux_plugin.h>\n#include <tray_manager/tray_manager_plugin.h>\n#include <url_launcher_linux/url_launcher_plugin.h>\n#include <window_manager/window_manager_plugin.h>\n\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"DesktopWebviewWindowPlugin\");\n  desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);\n  g_autoptr(FlPluginRegistrar) dynamic_color_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"DynamicColorPlugin\");\n  dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);\n  g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"FlutterVolumeControllerPlugin\");\n  flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar);\n  g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"MediaKitLibsLinuxPlugin\");\n  media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);\n  g_autoptr(FlPluginRegistrar) media_kit_video_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"MediaKitVideoPlugin\");\n  media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);\n  g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"ScreenRetrieverLinuxPlugin\");\n  screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);\n  g_autoptr(FlPluginRegistrar) tray_manager_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"TrayManagerPlugin\");\n  tray_manager_plugin_register_with_registrar(tray_manager_registrar);\n  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"UrlLauncherPlugin\");\n  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);\n  g_autoptr(FlPluginRegistrar) window_manager_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"WindowManagerPlugin\");\n  window_manager_plugin_register_with_registrar(window_manager_registrar);\n}\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter_linux/flutter_linux.h>\n\n// Registers Flutter plugins.\nvoid fl_register_plugins(FlPluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  desktop_webview_window\n  dynamic_color\n  flutter_volume_controller\n  media_kit_libs_linux\n  media_kit_video\n  screen_retriever_linux\n  tray_manager\n  url_launcher_linux\n  window_manager\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#include <sys/statvfs.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n  FlMethodChannel* intent_method_channel;\n  FlMethodChannel* storage_method_channel;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\nstatic FlMethodResponse* is_running_on_x11() {\n  GdkDisplay* display = gdk_display_get_default();\n  gboolean is_x11 = GDK_IS_X11_DISPLAY(display);\n  g_autoptr(FlValue) result = fl_value_new_bool(is_x11);\n  return FL_METHOD_RESPONSE(fl_method_success_response_new(result));\n}\n\nstatic void storage_method_call_handler(FlMethodChannel* channel,\n                                         FlMethodCall* method_call,\n                                         gpointer user_data) {\n  g_autoptr(FlMethodResponse) response = nullptr;\n  if (strcmp(fl_method_call_get_name(method_call), \"getAvailableStorage\") == 0) {\n    const gchar* path = \"/\";\n    FlValue* args = fl_method_call_get_args(method_call);\n    if (fl_value_get_type(args) == FL_VALUE_TYPE_MAP) {\n      FlValue* path_value = fl_value_lookup_string(args, \"path\");\n      if (path_value != nullptr && fl_value_get_type(path_value) == FL_VALUE_TYPE_STRING) {\n        path = fl_value_get_string(path_value);\n      }\n    }\n\n    struct statvfs stat;\n    if (statvfs(path, &stat) == 0) {\n      gint64 available = (gint64)stat.f_bavail * (gint64)stat.f_frsize;\n      g_autoptr(FlValue) result = fl_value_new_int(available);\n      response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));\n    } else {\n      g_autoptr(FlValue) result = fl_value_new_int(-1);\n      response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));\n    }\n  } else {\n    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());\n  }\n\n  g_autoptr(GError) error = nullptr;\n  if (!fl_method_call_respond(method_call, response, &error)) {\n    g_warning(\"Failed to send response: %s\", error->message);\n  }\n}\n\nstatic void intent_method_call_handler(FlMethodChannel* channel,\n                                        FlMethodCall* method_call,\n                                        gpointer user_data) {\n  g_autoptr(FlMethodResponse) response = nullptr;\n  if (strcmp(fl_method_call_get_name(method_call), \"isRunningOnX11\") == 0) {\n    response = is_running_on_x11();\n  } else {\n    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());\n  }\n\n  g_autoptr(GError) error = nullptr;\n  if (!fl_method_call_respond(method_call, response, &error)) {\n    g_warning(\"Failed to send response: %s\", error->message);\n  }\n}\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"kazumi\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"kazumi\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();\n  self->intent_method_channel = fl_method_channel_new(\n  fl_engine_get_binary_messenger(fl_view_get_engine(view)), \"com.predidit.kazumi/intent\", FL_METHOD_CODEC(codec));\n  fl_method_channel_set_method_call_handler(\n      self->intent_method_channel, intent_method_call_handler, self, nullptr);\n\n  self->storage_method_channel = fl_method_channel_new(\n      fl_engine_get_binary_messenger(fl_view_get_engine(view)), \"com.predidit.kazumi/storage\", FL_METHOD_CODEC(codec));\n  fl_method_channel_set_method_call_handler(\n      self->storage_method_channel, storage_method_call_handler, self, nullptr);\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GApplication::startup.\nstatic void my_application_startup(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application startup.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->startup(application);\n}\n\n// Implements GApplication::shutdown.\nstatic void my_application_shutdown(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application shutdown.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  g_clear_object(&self->intent_method_channel);\n  g_clear_object(&self->storage_method_channel);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_APPLICATION_CLASS(klass)->startup = my_application_startup;\n  G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/GeneratedPluginRegistrant.swift",
    "content": "//\n//  Generated file. Do not edit.\n//\n\nimport FlutterMacOS\nimport Foundation\n\nimport connectivity_plus\nimport dynamic_color\nimport flutter_inappwebview_macos\nimport flutter_volume_controller\nimport media_kit_libs_macos_video\nimport media_kit_video\nimport package_info_plus\nimport path_provider_foundation\nimport screen_retriever_macos\nimport shared_preferences_foundation\nimport sqflite_darwin\nimport tray_manager\nimport url_launcher_macos\nimport wakelock_plus\nimport window_manager\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: \"ConnectivityPlusPlugin\"))\n  DynamicColorPlugin.register(with: registry.registrar(forPlugin: \"DynamicColorPlugin\"))\n  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: \"InAppWebViewFlutterPlugin\"))\n  FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: \"FlutterVolumeControllerPlugin\"))\n  MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: \"MediaKitLibsMacosVideoPlugin\"))\n  MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: \"MediaKitVideoPlugin\"))\n  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: \"FPPPackageInfoPlusPlugin\"))\n  PathProviderPlugin.register(with: registry.registrar(forPlugin: \"PathProviderPlugin\"))\n  ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: \"ScreenRetrieverMacosPlugin\"))\n  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: \"SharedPreferencesPlugin\"))\n  SqflitePlugin.register(with: registry.registrar(forPlugin: \"SqflitePlugin\"))\n  TrayManagerPlugin.register(with: registry.registrar(forPlugin: \"TrayManagerPlugin\"))\n  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: \"UrlLauncherPlugin\"))\n  WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: \"WakelockPlusMacosPlugin\"))\n  WindowManagerPlugin.register(with: registry.registrar(forPlugin: \"WindowManagerPlugin\"))\n}\n"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\nimport SwiftUI\nimport AVKit\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n    override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n        return true\n    }\n    \n    override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n        return true\n    }\n    \n    var playerView: AVPlayerView!\n    var player: AVPlayer?\n    var videoUrl: URL?\n    var httpReferer: String = \"\"\n    var menuChannel: FlutterMethodChannel?\n\n    \n    override func applicationDidFinishLaunching(_ notification: Notification) {\n        setMenuEnabled(menu: \"Player\", enable: false)\n        let controller : FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController\n        let channel = FlutterMethodChannel.init(name: \"com.predidit.kazumi/intent\", binaryMessenger: controller.engine.binaryMessenger)\n        self.menuChannel = FlutterMethodChannel.init(name: \"com.predidit.kazumi/appmenu\",binaryMessenger: controller.engine.binaryMessenger)\n        channel.setMethodCallHandler({\n            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in\n            if call.method == \"openWithReferer\" {\n                guard let args = call.arguments else { return }\n                if let myArgs = args as? [String: Any],\n                   let url = myArgs[\"url\"] as? String,\n                   let referer = myArgs[\"referer\"] as? String {\n                    self.openVideoWithReferer(url: url, referer: referer)\n                }\n                result(nil)\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        });\n\n        let storageChannel = FlutterMethodChannel.init(name: \"com.predidit.kazumi/storage\", binaryMessenger: controller.engine.binaryMessenger)\n        storageChannel.setMethodCallHandler({\n            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in\n            if call.method == \"getAvailableStorage\" {\n                do {\n                    let attrs = try FileManager.default.attributesOfFileSystem(\n                        forPath: NSHomeDirectory()\n                    )\n                    if let freeSize = attrs[.systemFreeSize] as? Int64 {\n                        result(freeSize)\n                    } else {\n                        result(-1)\n                    }\n                } catch {\n                    result(-1)\n                }\n            } else {\n                result(FlutterMethodNotImplemented)\n            }\n        });\n        self.menuChannel?.setMethodCallHandler({call,result in\n            switch call.method {\n            case \"setMenuEnabled\":\n                guard let args = call.arguments as? [String: Any],\n                    let menu = args[\"menu\"] as? String,\n                    let enable = args[\"enable\"] as? Bool else {\n                    result(FlutterMethodNotImplemented)\n                    return }\n                self.setMenuEnabled(menu: menu, enable: enable)\n                result(nil)\n            default:\n                result(FlutterMethodNotImplemented)\n            }\n        });\n    }\n    \n    func findApplicationsByMimeType() -> [URL] {\n        let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(\"temp.mp4\")\n        \n        FileManager.default.createFile(atPath: tempFileURL.path, contents: nil, attributes: nil)\n        \n        if #available(macOS 12.0, *) {\n            let listOfExternalApps = NSWorkspace.shared.urlsForApplications(toOpen: tempFileURL)\n            if FileManager.default.fileExists(atPath: tempFileURL.path) {\n                do {\n                    try FileManager.default.removeItem(atPath: tempFileURL.path)\n                } catch {\n                    print(\"Delete error: \\(error.localizedDescription)\")\n                }\n            }\n            return listOfExternalApps\n        } else {\n            if FileManager.default.fileExists(atPath: tempFileURL.path) {\n                do {\n                    try FileManager.default.removeItem(atPath: tempFileURL.path)\n                } catch {\n                    print(\"Delete error: \\(error.localizedDescription)\")\n                }\n            }\n            return []\n        }\n    }\n    \n    private func openVideoWithReferer(url: String, referer: String) {\n        videoUrl = URL(string: url)\n        httpReferer = referer\n        \n        let selectMenu = NSMenu()\n        let appLists = findApplicationsByMimeType()\n        \n        /* AVPlayer menu item start */\n        let menuItem = NSMenuItem()\n        menuItem.attributedTitle = NSAttributedString(string: \"AVPlayer\", attributes: [.font: NSFont.systemFont(ofSize: 14)])\n        menuItem.action = #selector(openWithAVPlayer)\n        \n        let icon = NSWorkspace.shared.icon(forFile: \"/System/Applications/Preview.app\")\n        icon.size = NSSize(width: 16, height: 16)\n        menuItem.image = icon\n        \n        selectMenu.addItem(menuItem)\n        /* AVPlayer menu item end */\n        \n        /* Applications menu item start */\n        for appList in appLists {\n            let appBundle = Bundle(url: appList)\n            let appName = appBundle?.infoDictionary?[\"CFBundleName\"] as? String ?? \"\"\n            if appName == \"QuickTime Player\" || appName == \"Books\" {\n                continue\n            }\n            \n            let menuItem = NSMenuItem()\n            menuItem.attributedTitle = NSAttributedString(string: \"\\(appName).app\", attributes: [.font: NSFont.systemFont(ofSize: 14)])\n            if appName == \"VLC\" {\n                menuItem.action = #selector(openWithVLC(_:))\n            } else {\n                menuItem.action = #selector(openWithSelectedApp(_:))\n            }\n            menuItem.representedObject = \"/Applications/\\(appName).app/Contents/MacOS/\\(appName)\"\n            \n            let icon = NSWorkspace.shared.icon(forFile: \"/Applications/\\(appName).app\")\n            icon.size = NSSize(width: 16, height: 16)\n            menuItem.image = icon\n\n            selectMenu.addItem(menuItem)\n        }\n        /* Applications menu item end */\n        \n        selectMenu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)\n    }\n    \n    @objc func openWithAVPlayer () {\n        let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 1280, height: 860),\n                              styleMask: [.titled, .closable, .resizable],\n                              backing: .buffered, defer: false)\n        window.center()\n        window.makeKeyAndOrderFront(nil)\n        window.isReleasedWhenClosed = false\n        playerView = AVPlayerView(frame: window.contentView!.bounds)\n        playerView.autoresizingMask = [.width, .height]\n        window.contentView?.addSubview(playerView)\n        window.delegate = self\n        \n        let headers: [String: String] = [\n            \"Referer\": httpReferer,\n        ]\n        let asset = AVURLAsset(url: videoUrl!, options: [\"AVURLAssetHTTPHeaderFieldsKey\": headers])\n        let playerItem = AVPlayerItem(asset: asset)\n        player = AVPlayer(playerItem: playerItem)\n        playerView.player = player\n        playerView.player?.play()\n    }\n    \n    @objc func openWithSelectedApp (_ sender: NSMenuItem) {\n        if !httpReferer.isEmpty {\n            let alert = NSAlert()\n            alert.messageText = \"打开应用失败\"\n            alert.informativeText = \"该应用不支持 Referer 请求头，打开失败。请使用 AVPlayer/VLC 打开或更换规则。\"\n            alert.runModal()\n            return\n        }\n        if let selectedApp = sender.representedObject {\n            let process = Process()\n            process.launchPath = selectedApp as? String\n            process.arguments = [videoUrl!.absoluteString]\n\n            do {\n                try process.run()\n            } catch {\n                print(\"Failed to open app: \\(error)\")\n            }\n        }\n    }\n    \n    @objc func openWithVLC (_ sender: NSMenuItem) {\n        if let selectedApp = sender.representedObject {\n            let process = Process()\n            process.launchPath = selectedApp as? String\n            process.arguments = [videoUrl!.absoluteString, \":http-referrer=\" + httpReferer]\n\n            do {\n                try process.run()\n            } catch {\n                print(\"Failed to open app: \\(error)\")\n            }\n        }\n    }\n\n    var isPlayerActive: Bool = false\n\n    func sendToFlutter(_ command: String){\n        menuChannel?.invokeMethod(command, arguments: nil)\n    }\n    @IBAction func menuPlayPause(_ sender: Any) { sendToFlutter(\"playorpause\") }\n    @IBAction func menuNext(_ sender: Any) { sendToFlutter(\"next\") }\n    @IBAction func menuPrevious(_ sender: Any) { sendToFlutter(\"prev\") }\n    @IBAction func menuForward(_ sender: Any) { sendToFlutter(\"forward\") }\n    @IBAction func menuRewind(_ sender: Any) { sendToFlutter(\"rewind\") }\n    @IBAction func menuVolumeUp(_ sender: Any) { sendToFlutter(\"volumeup\") }\n    @IBAction func menuVolumeDown(_ sender: Any) { sendToFlutter(\"volumedown\") }\n    @IBAction func menuToggleMute(_ sender: Any) { sendToFlutter(\"togglemute\") }\n    @IBAction func menuToggleDanmaku(_ sender: Any) { sendToFlutter(\"toggledanmaku\") }\n    @IBAction func menuSkip(_ sender: Any) { sendToFlutter(\"skip\") }\n    @IBAction func menuSpeed1(_ sender: Any) { sendToFlutter(\"speed1\") }\n    @IBAction func menuSpeed2(_ sender: Any) { sendToFlutter(\"speed2\") }\n    @IBAction func menuSpeed3(_ sender: Any) { sendToFlutter(\"speed3\") }\n    @IBAction func menuSpeedUp(_ sender: Any) { sendToFlutter(\"speedup\") }\n    @IBAction func menuSpeedDown(_ sender: Any) { sendToFlutter(\"speeddown\") }\n\n    func setMenuEnabled(menu: String, enable: Bool) {\n        if let menuItem = NSApp.mainMenu?.items.first(where: { $0.identifier?.rawValue == menu }) {\n            menuItem.isEnabled = enable\n        }\n    }\n}\n\nextension AppDelegate: NSWindowDelegate {\n    func windowWillClose(_ notification: Notification) {\n        player?.pause()\n        player = nil\n    }\n}\n"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n    \"info\": {\n        \"version\": 1,\n        \"author\": \"xcode\"\n    },\n    \"images\": [\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_16.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"16x16\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_32.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_32.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"32x32\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_64.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_128.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"128x128\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_256.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_256.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"256x256\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_512.png\",\n            \"scale\": \"2x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_512.png\",\n            \"scale\": \"1x\"\n        },\n        {\n            \"size\": \"512x512\",\n            \"idiom\": \"mac\",\n            \"filename\": \"app_icon_1024.png\",\n            \"scale\": \"2x\"\n        }\n    ]\n}"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"24127\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"24127\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Kazumi\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Player\" identifier=\"PlayerMenu\" id=\"vQb-zP-A4Q\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Player\" id=\"ufy-gc-bv9\">\n                        <items>\n                            <menuItem title=\"Play / Pause\" image=\"playpause.fill\" catalog=\"system\" id=\"Yz8-Qq-Cit\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuPlayPause:\" target=\"-1\" id=\"NHI-W0-zAE\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Forward\" image=\"forward.fill\" catalog=\"system\" id=\"1oV-tb-v1I\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuForward:\" target=\"-1\" id=\"lyl-Fh-Jp6\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Rewind\" image=\"backward.fill\" catalog=\"system\" id=\"hfw-bJ-3tF\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuRewind:\" target=\"-1\" id=\"f1W-5c-HuA\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Next\" image=\"forward.end.fill\" catalog=\"system\" id=\"cVr-GM-2GR\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuNext:\" target=\"-1\" id=\"hdP-70-tVI\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Previous\" image=\"backward.end.fill\" catalog=\"system\" id=\"Ox4-Od-VDn\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuPrevious:\" target=\"-1\" id=\"ydf-b2-Mwz\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Skip\" image=\"NSTouchBarSkipAhead30SecondsTemplate\" id=\"jFI-nV-KPO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuSkip:\" target=\"-1\" id=\"MHM-Mm-dSL\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"ruh-Xu-tvB\"/>\n                            <menuItem title=\"Volume Up\" image=\"speaker.plus.fill\" catalog=\"system\" id=\"o2L-YK-Znd\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuVolumeUp:\" target=\"-1\" id=\"pqi-ks-VQC\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Volume Down\" image=\"speaker.minus.fill\" catalog=\"system\" id=\"CEJ-Pz-JnN\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuVolumeDown:\" target=\"-1\" id=\"C4z-PB-KaE\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Toggle Mute\" image=\"speaker.slash.fill\" catalog=\"system\" id=\"0go-7w-Agj\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuToggleMute:\" target=\"-1\" id=\"dna-bl-RPf\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"dKr-8P-RX9\"/>\n                            <menuItem title=\"Toggle Danmaku\" id=\"lmU-yX-0QG\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"menuToggleDanmaku:\" target=\"-1\" id=\"v0C-DN-WQV\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Speed\" id=\"weg-Na-dVX\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speed\" id=\"FYI-7j-0Xf\">\n                                    <items>\n                                        <menuItem title=\"1x\" id=\"bfm-UB-X5j\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"menuSpeed1:\" target=\"-1\" id=\"aRw-nP-8ho\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"2x\" id=\"jBw-Xy-dFw\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"menuSpeed2:\" target=\"-1\" id=\"Agr-AV-U8w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"3x\" id=\"kW7-vW-l12\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"menuSpeed3:\" target=\"-1\" id=\"EsI-b2-hxk\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Speed Up\" image=\"gauge.with.dots.needle.67percent\" catalog=\"system\" id=\"kCQ-C0-LpW\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"menuSpeedUp:\" target=\"-1\" id=\"J2g-cH-8Da\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Speed Down\" image=\"gauge.with.dots.needle.33percent\" catalog=\"system\" id=\"V9W-wp-A8v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"menuSpeedDown:\" target=\"-1\" id=\"gqy-pK-cyG\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" titlebarAppearsTransparent=\"YES\" titleVisibility=\"hidden\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Kazumi\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\" fullSizeContentView=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"1680\" height=\"1019\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n            <point key=\"canvasLocation\" x=\"139\" y=\"144\"/>\n        </window>\n    </objects>\n    <resources>\n        <image name=\"NSTouchBarSkipAhead30SecondsTemplate\" width=\"20\" height=\"22\"/>\n        <image name=\"backward.end.fill\" catalog=\"system\" width=\"15\" height=\"12\"/>\n        <image name=\"backward.fill\" catalog=\"system\" width=\"20\" height=\"12\"/>\n        <image name=\"forward.end.fill\" catalog=\"system\" width=\"15\" height=\"12\"/>\n        <image name=\"forward.fill\" catalog=\"system\" width=\"20\" height=\"12\"/>\n        <image name=\"gauge.with.dots.needle.33percent\" catalog=\"system\" width=\"15\" height=\"15\"/>\n        <image name=\"gauge.with.dots.needle.67percent\" catalog=\"system\" width=\"15\" height=\"15\"/>\n        <image name=\"playpause.fill\" catalog=\"system\" width=\"22\" height=\"12\"/>\n        <image name=\"speaker.minus.fill\" catalog=\"system\" width=\"19\" height=\"14\"/>\n        <image name=\"speaker.plus.fill\" catalog=\"system\" width=\"19\" height=\"14\"/>\n        <image name=\"speaker.slash.fill\" catalog=\"system\" width=\"14\" height=\"16\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = kazumi\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.files.downloads.read-write</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<true/>\n\t</dict>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\nimport window_manager\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController()\n    self.backgroundColor = NSColor.clear\n    flutterViewController.backgroundColor = NSColor.clear\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n\n  override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {\n    super.order(place, relativeTo: otherWin)\n    hiddenWindowAtLaunch()\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.files.downloads.read-write</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/en-GB.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n"
  },
  {
    "path": "macos/Runner/en.lproj/MainMenu.strings",
    "content": "\n/* Class = \"NSMenuItem\"; title = \"APP_NAME\"; ObjectID = \"1Xt-HY-uBw\"; */\n\"1Xt-HY-uBw.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"Transformations\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"Speech\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"Enter Full Screen\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"Quit APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"About APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"Redo\";\n\n/* Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\"; */\n\"AYu-sK-qS6.title\" = \"Main Menu\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"Help\";\n\n/* Class = \"NSMenuItem\"; title = \"View\"; ObjectID = \"H8h-7b-M4v\"; */\n\"H8h-7b-M4v.title\" = \"View\";\n\n/* Class = \"NSMenu\"; title = \"View\"; ObjectID = \"HyV-fh-RgO\"; */\n\"HyV-fh-RgO.title\" = \"View\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"Show All\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"Bring All to Front\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"Minimize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"Hide APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"Stop Speaking\";\n\n/* Class = \"NSWindow\"; title = \"APP_NAME\"; ObjectID = \"QvC-M9-y7g\"; */\n\"QvC-M9-y7g.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"Zoom\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"Select All\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"Window\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"Capitalize\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"Hide Others\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"Edit\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"Start Speaking\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"Window\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"Transformations\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"Make Lower Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"Undo\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"Paste\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"Services\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"Delete\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"Help\";\n\n/* Class = \"NSMenu\"; title = \"APP_NAME\"; ObjectID = \"uQy-DD-JDr\"; */\n\"uQy-DD-JDr.title\" = \"APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"Cut\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"Make Upper Case\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"Copy\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"Speech\";\n"
  },
  {
    "path": "macos/Runner/zh-Hans.lproj/MainMenu.strings",
    "content": "/* Class = \"NSMenuItem\"; title = \"Toggle Mute\"; ObjectID = \"0go-7w-Agj\"; */\n\"0go-7w-Agj.title\" = \"切换静音\";\n\n/* Class = \"NSMenuItem\"; title = \"Forward\"; ObjectID = \"1oV-tb-v1I\"; */\n\"1oV-tb-v1I.title\" = \"快进\";\n\n/* Class = \"NSMenuItem\"; title = \"Transformations\"; ObjectID = \"2oI-Rn-ZJC\"; */\n\"2oI-Rn-ZJC.title\" = \"转换\";\n\n/* Class = \"NSMenu\"; title = \"Speech\"; ObjectID = \"3rS-ZA-NoH\"; */\n\"3rS-ZA-NoH.title\" = \"语音\";\n\n/* Class = \"NSMenuItem\"; title = \"Enter Full Screen\"; ObjectID = \"4J7-dP-txa\"; */\n\"4J7-dP-txa.title\" = \"进入全屏幕\";\n\n/* Class = \"NSMenuItem\"; title = \"Quit APP_NAME\"; ObjectID = \"4sb-4s-VLi\"; */\n\"4sb-4s-VLi.title\" = \"退出APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"About APP_NAME\"; ObjectID = \"5kV-Vb-QxS\"; */\n\"5kV-Vb-QxS.title\" = \"关于APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Edit\"; ObjectID = \"5QF-Oa-p0T\"; */\n\"5QF-Oa-p0T.title\" = \"编辑\";\n\n/* Class = \"NSMenuItem\"; title = \"Redo\"; ObjectID = \"6dh-zS-Vam\"; */\n\"6dh-zS-Vam.title\" = \"重做\";\n\n/* Class = \"NSMenuItem\"; title = \"Window\"; ObjectID = \"aUF-d1-5bR\"; */\n\"aUF-d1-5bR.title\" = \"窗口\";\n\n/* Class = \"NSMenu\"; title = \"Transformations\"; ObjectID = \"c8a-y6-VQd\"; */\n\"c8a-y6-VQd.title\" = \"转换\";\n\n/* Class = \"NSMenuItem\"; title = \"Volume Down\"; ObjectID = \"CEJ-Pz-JnN\"; */\n\"CEJ-Pz-JnN.title\" = \"音量减\";\n\n/* Class = \"NSMenuItem\"; title = \"Next\"; ObjectID = \"cVr-GM-2GR\"; */\n\"cVr-GM-2GR.title\" = \"下一集\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Lower Case\"; ObjectID = \"d9M-CD-aMd\"; */\n\"d9M-CD-aMd.title\" = \"转换为小写\";\n\n/* Class = \"NSMenuItem\"; title = \"Undo\"; ObjectID = \"dRJ-4n-Yzg\"; */\n\"dRJ-4n-Yzg.title\" = \"撤销\";\n\n/* Class = \"NSMenuItem\"; title = \"Help\"; ObjectID = \"EPT-qC-fAb\"; */\n\"EPT-qC-fAb.title\" = \"帮助\";\n\n/* Class = \"NSMenu\"; title = \"Speed\"; ObjectID = \"FYI-7j-0Xf\"; */\n\"FYI-7j-0Xf.title\" = \"倍速\";\n\n/* Class = \"NSMenuItem\"; title = \"Paste\"; ObjectID = \"gVA-U4-sdL\"; */\n\"gVA-U4-sdL.title\" = \"粘贴\";\n\n/* Class = \"NSMenuItem\"; title = \"Rewind\"; ObjectID = \"hfw-bJ-3tF\"; */\n\"hfw-bJ-3tF.title\" = \"快退\";\n\n/* Class = \"NSMenu\"; title = \"Services\"; ObjectID = \"hz9-B4-Xy5\"; */\n\"hz9-B4-Xy5.title\" = \"服务\";\n\n/* Class = \"NSMenuItem\"; title = \"Skip\"; ObjectID = \"jFI-nV-KPO\"; */\n\"jFI-nV-KPO.title\" = \"跳过\";\n\n/* Class = \"NSMenuItem\"; title = \"Speed Up\"; ObjectID = \"kCQ-C0-LpW\"; */\n\"kCQ-C0-LpW.title\" = \"倍速加\";\n\n/* Class = \"NSMenuItem\"; title = \"Show All\"; ObjectID = \"Kd2-mp-pUS\"; */\n\"Kd2-mp-pUS.title\" = \"显示全部\";\n\n/* Class = \"NSMenuItem\"; title = \"Bring All to Front\"; ObjectID = \"LE2-aR-0XJ\"; */\n\"LE2-aR-0XJ.title\" = \"前置全部窗口\";\n\n/* Class = \"NSMenuItem\"; title = \"Toggle Danmaku\"; ObjectID = \"lmU-yX-0QG\"; */\n\"lmU-yX-0QG.title\" = \"弹幕开关\";\n\n/* Class = \"NSMenuItem\"; title = \"Services\"; ObjectID = \"NMo-om-nkz\"; */\n\"NMo-om-nkz.title\" = \"服务\";\n\n/* Class = \"NSMenuItem\"; title = \"Volume Up\"; ObjectID = \"o2L-YK-Znd\"; */\n\"o2L-YK-Znd.title\" = \"音量加\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide APP_NAME\"; ObjectID = \"Olw-nP-bQN\"; */\n\"Olw-nP-bQN.title\" = \"隐藏APP_NAME\";\n\n/* Class = \"NSMenuItem\"; title = \"Previous\"; ObjectID = \"Ox4-Od-VDn\"; */\n\"Ox4-Od-VDn.title\" = \"上一集\";\n\n/* Class = \"NSMenuItem\"; title = \"Minimize\"; ObjectID = \"OY7-WF-poV\"; */\n\"OY7-WF-poV.title\" = \"最小化\";\n\n/* Class = \"NSMenuItem\"; title = \"Stop Speaking\"; ObjectID = \"Oyz-dy-DGm\"; */\n\"Oyz-dy-DGm.title\" = \"停止讲话\";\n\n/* Class = \"NSMenuItem\"; title = \"Delete\"; ObjectID = \"pa3-QI-u2k\"; */\n\"pa3-QI-u2k.title\" = \"删除\";\n\n/* Class = \"NSMenuItem\"; title = \"Zoom\"; ObjectID = \"R4o-n2-Eq4\"; */\n\"R4o-n2-Eq4.title\" = \"缩放\";\n\n/* Class = \"NSMenu\"; title = \"Help\"; ObjectID = \"rJ0-wn-3NY\"; */\n\"rJ0-wn-3NY.title\" = \"帮助\";\n\n/* Class = \"NSMenuItem\"; title = \"Select All\"; ObjectID = \"Ruw-6m-B2m\"; */\n\"Ruw-6m-B2m.title\" = \"全选\";\n\n/* Class = \"NSMenu\"; title = \"Window\"; ObjectID = \"Td7-aD-5lo\"; */\n\"Td7-aD-5lo.title\" = \"窗口\";\n\n/* Class = \"NSMenuItem\"; title = \"Capitalize\"; ObjectID = \"UEZ-Bs-lqG\"; */\n\"UEZ-Bs-lqG.title\" = \"大写\";\n\n/* Class = \"NSMenu\"; title = \"Player\"; ObjectID = \"ufy-gc-bv9\"; */\n\"ufy-gc-bv9.title\" = \"播放器\";\n\n/* Class = \"NSMenuItem\"; title = \"Cut\"; ObjectID = \"uRl-iY-unG\"; */\n\"uRl-iY-unG.title\" = \"剪切\";\n\n/* Class = \"NSMenuItem\"; title = \"Speed Down\"; ObjectID = \"V9W-wp-A8v\"; */\n\"V9W-wp-A8v.title\" = \"倍速减\";\n\n/* Class = \"NSMenuItem\"; title = \"Hide Others\"; ObjectID = \"Vdr-fp-XzO\"; */\n\"Vdr-fp-XzO.title\" = \"隐藏其他应用\";\n\n/* Class = \"NSMenuItem\"; title = \"Make Upper Case\"; ObjectID = \"vmV-6d-7jI\"; */\n\"vmV-6d-7jI.title\" = \"转换为大写\";\n\n/* Class = \"NSMenuItem\"; title = \"Player\"; ObjectID = \"vQb-zP-A4Q\"; */\n\"vQb-zP-A4Q.title\" = \"播放器\";\n\n/* Class = \"NSMenu\"; title = \"Edit\"; ObjectID = \"W48-6f-4Dl\"; */\n\"W48-6f-4Dl.title\" = \"编辑\";\n\n/* Class = \"NSMenuItem\"; title = \"Speed\"; ObjectID = \"weg-Na-dVX\"; */\n\"weg-Na-dVX.title\" = \"倍速\";\n\n/* Class = \"NSMenuItem\"; title = \"Copy\"; ObjectID = \"x3v-GG-iWU\"; */\n\"x3v-GG-iWU.title\" = \"复制\";\n\n/* Class = \"NSMenuItem\"; title = \"Speech\"; ObjectID = \"xrE-MZ-jX0\"; */\n\"xrE-MZ-jX0.title\" = \"语音\";\n\n/* Class = \"NSMenuItem\"; title = \"Start Speaking\"; ObjectID = \"Ynk-f8-cLZ\"; */\n\"Ynk-f8-cLZ.title\" = \"开始讲话\";\n\n/* Class = \"NSMenuItem\"; title = \"Play / Pause\"; ObjectID = \"Yz8-Qq-Cit\"; */\n\"Yz8-Qq-Cit.title\" = \"播放 / 暂停\";\n\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC10EC2044A3C60003C045;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t08D6925B2E3A29C100F22E48 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"zh-Hans\"; path = \"zh-Hans.lproj/MainMenu.strings\"; sourceTree = \"<group>\"; };\n\t\t08D6925D2E3A29CC00F22E48 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = \"<group>\"; };\n\t\t08D6925F2E3A29CE00F22E48 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = \"en-GB\"; path = \"en-GB.lproj/MainMenu.strings\"; sourceTree = \"<group>\"; };\n\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* Kazumi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Kazumi.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t331C80D2294CF70F00263BE5 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C80D6294CF71000263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t331C80D6294CF71000263BE5 /* RunnerTests */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* Kazumi.app */,\n\t\t\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C80D4294CF70F00263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C80D1294CF70F00263BE5 /* Sources */,\n\t\t\t\t331C80D2294CF70F00263BE5 /* Frameworks */,\n\t\t\t\t331C80D3294CF70F00263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* Kazumi.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C80D4294CF70F00263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 33CC10EC2044A3C60003C045;\n\t\t\t\t\t};\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = \"zh-Hans\";\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\tBase,\n\t\t\t\ten,\n\t\t\t\t\"zh-Hans\",\n\t\t\t\t\"en-GB\",\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t331C80D4294CF70F00263BE5 /* RunnerTests */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C80D3294CF70F00263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C80D1294CF70F00263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC10EC2044A3C60003C045 /* Runner */;\n\t\t\ttargetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t\t08D6925B2E3A29C100F22E48 /* zh-Hans */,\n\t\t\t\t08D6925D2E3A29CC00F22E48 /* en */,\n\t\t\t\t08D6925F2E3A29CE00F22E48 /* en-GB */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t331C80DB294CF71000263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C80DC294CF71000263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C80DD294CF71000263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_NAME = Kazumi;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_NAME = Kazumi;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_NAME = Kazumi;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C80DB294CF71000263BE5 /* Debug */,\n\t\t\t\t331C80DC294CF71000263BE5 /* Release */,\n\t\t\t\t331C80DD294CF71000263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"Kazumi.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"Kazumi.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C80D4294CF70F00263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"Kazumi.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"Kazumi.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/RunnerTests/RunnerTests.swift",
    "content": "import FlutterMacOS\nimport Cocoa\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: kazumi\ndescription: \"A new Flutter project.\"\n# The following line prevents the package from being accidentally published to\n# pub.dev using `flutter pub publish`. This is preferred for private packages.\npublish_to: \"none\" # Remove this line if you wish to publish to pub.dev\n\n# The following defines the version and build number for your application.\n# A version number is three numbers separated by dots, like 1.2.43\n# followed by an optional build number separated by a +.\n# Both the version and the builder number may be overridden in flutter\n# build by specifying --build-name and --build-number, respectively.\n# In Android, build-name is used as versionName while build-number used as versionCode.\n# Read more about Android versioning at https://developer.android.com/studio/publish/versioning\n# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.\n# Read more about iOS versioning at\n# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html\n# In Windows, build-name is used as the major, minor, and patch parts\n# of the product and file versions while build-number is used as the build suffix.\nversion: 2.0.5+20005\n\nenvironment:\n  sdk: \">=3.3.4 <4.0.0\"\n  flutter: 3.41.5\n\n# Dependencies specify other packages that your package needs in order to work.\n# To automatically upgrade your package dependencies to the latest versions\n# consider running `flutter pub upgrade --major-versions`. Alternatively,\n# dependencies can be manually updated by changing the version numbers below to\n# the latest version available on pub.dev. To see which dependencies have newer\n# versions available, run `flutter pub outdated`.\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  cupertino_icons: ^1.0.6\n  flutter_modular: ^6.3.4\n  mobx: ^2.6.0\n  flutter_mobx: ^2.3.0\n  dio: ^5.0.0\n  cookie_jar: ^4.0.9\n  connectivity_plus: ^6.0.5\n\n  path_provider: ^2.1.5\n  hive_ce: ^2.16.0\n  hive_ce_flutter: ^2.3.3\n  cached_network_image: ^3.4.1\n  card_settings_ui: ^2.0.1\n\n  # fvp: ^0.28.0\n  # video_player: ^2.9.1\n\n  flutter_volume_controller: ^1.3.2\n  audio_video_progress_bar: ^2.0.2\n  dynamic_color: ^1.8.1\n  provider: ^6.1.2\n  flutter_displaymode: ^0.6.0\n\n  url_launcher: ^6.3.0\n  window_manager: ^0.5.1\n  xpath_selector: ^3.0.2\n  xpath_selector_html_parser: ^3.0.1\n\n  canvas_danmaku: ^0.3.1\n  webdav_client: ^1.2.2\n  tray_manager: ^0.5.0\n  dlna_dart: ^0.0.8\n  logger: ^2.6.2\n  flutter_rating_bar: ^4.0.1\n  scrollview_observer: ^1.22.0\n  saver_gallery: ^4.1.0\n  screen_brightness_android: ^2.1.3\n  screen_brightness_ios: ^2.1.2\n  screen_brightness_ohos: ^2.1.2\n  screen_brightness_platform_interface: ^2.1.0\n  synchronized: any\n  flutter_svg: ^2.2.3\n  antlr4: ^4.13.2\n  fl_chart: ^1.1.1\n  flutter_foreground_task: ^9.0.0\n  skeletonizer: ^2.1.2\n  flutter_inappwebview_platform_interface: ^1.3.0+1\n  flutter_inappwebview_ios: ^1.1.2\n  flutter_inappwebview_macos: ^1.1.2\n  flutter_inappwebview_android: ^1.1.3\n  upgrader: ^12.3.0\n  open_filex: ^4.7.0\n  html: any\n  material_color_utilities: any\n  path: any\n  webview_windows:\n    git:\n      url: https://github.com/Predidit/flutter-webview-windows.git\n      ref: 1b9e83371356c04d8b0fb7ab1d20ebacf5cf9764\n  desktop_webview_window:\n    git:\n      url: https://github.com/Predidit/linux_webview_window.git\n      ref: 297b39532c426263d2fa9fde4d70d2b5bdfc8059\n  media_kit:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./media_kit\n  media_kit_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./media_kit_video\n  media_kit_libs_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/universal/media_kit_libs_video\n\ndependency_overrides:\n  media_kit_libs_linux:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/linux/media_kit_libs_linux\n  media_kit_libs_ios_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/ios/media_kit_libs_ios_video\n  media_kit_libs_android_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/android/media_kit_libs_android_video\n  media_kit_libs_windows_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/windows/media_kit_libs_windows_video\n  media_kit_libs_macos_video:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/macos/media_kit_libs_macos_video\n  media_kit_libs_ohos:\n    git:\n      url: https://github.com/Predidit/media-kit.git\n      ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970\n      path: ./libs/ohos/media_kit_libs_ohos\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n  # The \"flutter_lints\" package below contains a set of recommended lints to\n  # encourage good coding practices. The lint set provided by the package is\n  # activated in the `analysis_options.yaml` file located at the root of your\n  # package. See that file for information about deactivating specific lint\n  # rules and activating additional ones.\n  flutter_lints: ^6.0.0\n  build_runner: ^2.10.4\n  mobx_codegen: ^2.7.5\n  hive_ce_generator: ^1.10.0\n  flutter_launcher_icons: ^0.14.3\n  flutter_native_splash: ^2.4.3\n  msix: ^3.16.12\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\nflutter_launcher_icons:\n  android: true\n  ios: true\n  remove_alpha_ios: true\n  image_path: assets/images/logo/logo_android.png\n  image_path_android: assets/images/logo/logo_android.png\n  image_path_ios: assets/images/logo/logo_ios.png\n  adaptive_icon_background: \"#ffffff\"\n  adaptive_icon_foreground: assets/images/logo/logo_android.png\n  adaptive_icon_monochrome: assets/images/logo/logo_android.png\n  macos:\n    generate: true\n    image_path: assets/images/logo/logo_rounded.png\n  windows:\n    generate: true\n    image_path: assets/images/logo/logo_rounded.png\n    icon_size: 256 # min:48, max:256, default: 48\n\nflutter_native_splash:\n  android: false\n  ios: true\n  web: false\n  color_ios: \"#ffffff\"\n  color_dark_ios: \"#212121\"\n  image_ios: assets/images/logo/logo_ios.png\n  image_dark_ios: assets/images/logo/logo_ios.png\n\nmsix_config:\n  display_name: Kazumi\n  publisher: CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\n  logo_path: assets/images/logo/logo_rounded.png\n  sign_msix: false\n  install_certificate: false\n  build_windows: false\n\n# The following section is specific to Flutter packages.\nflutter:\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n  assets:\n    - assets/images/\n    - assets/plugins/\n    - assets/shaders/\n    - assets/images/logo/\n    - assets/statements/\n\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n  fonts:\n    - family: MI_Sans_Regular\n      fonts:\n        - asset: assets/fonts/MiSans-Regular.ttf"
  },
  {
    "path": "test/m3u8_parser_test.dart",
    "content": "﻿import 'package:flutter_test/flutter_test.dart';\nimport 'package:kazumi/utils/m3u8_parser.dart';\n\nvoid main() {\n  group('M3U8 Parser', () {\n    // ── Master playlist ──────────────────────────────────────────────────────\n    test('Test 1: Mux master playlist', () {\n      const content = r'''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-STREAM-INF:BANDWIDTH=246440,CODECS=\"avc1.42001e,mp4a.40.2\",RESOLUTION=320x184\nurl_2/193039199_mp4_h264_aac_ld_2.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=460560,CODECS=\"avc1.42001e,mp4a.40.2\",RESOLUTION=512x288\nurl_4/193039199_mp4_h264_aac_sd_4.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=836280,CODECS=\"avc1.42001f,mp4a.40.2\",RESOLUTION=848x480\nurl_6/193039199_mp4_h264_aac_480p_6.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2149280,CODECS=\"avc1.64001f,mp4a.40.2\",RESOLUTION=1280x720\nurl_0/193039199_mp4_h264_aac_hd_7.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=6221600,CODECS=\"avc1.640028,mp4a.40.2\",RESOLUTION=1920x1080\nurl_8/193039199_mp4_h264_aac_fhd_8.m3u8\n''';\n      const baseUrl = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';\n\n      expect(M3u8Parser.detectType(content), M3u8Type.master);\n\n      final master = M3u8Parser.parseMasterPlaylist(content, baseUrl);\n      expect(master.variants.length, 5);\n\n      final best = master.bestVariant;\n      expect(best.bandwidth, 6221600);\n      expect(best.resolution, '1920x1080');\n      expect(best.uri,\n          'https://test-streams.mux.dev/x36xhzz/url_8/193039199_mp4_h264_aac_fhd_8.m3u8');\n    });\n\n    // ── Media playlist with VOD + ENDLIST ────────────────────────────────────\n    test('Test 2: Mux media playlist (PLAYLIST-TYPE:VOD + ENDLIST) -> isVod=true', () {\n      const content = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:10.0,\nseg_00000.ts\n#EXTINF:10.0,\nseg_00001.ts\n#EXTINF:10.0,\nseg_00002.ts\n#EXTINF:9.6,\nseg_00003.ts\n#EXT-X-ENDLIST\n''';\n      const baseUrl =\n          'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8';\n\n      expect(M3u8Parser.detectType(content), M3u8Type.media);\n\n      final playlist = M3u8Parser.parseMediaPlaylist(content, baseUrl);\n      expect(playlist.isVod, isTrue);\n      expect(playlist.targetDuration, 10.0);\n      expect(playlist.segments.length, 4);\n      expect(playlist.segments.first.uri,\n          'https://test-streams.mux.dev/x36xhzz/url_0/seg_00000.ts');\n\n      expect(content.contains('#EXT-X-PLAYLIST-TYPE:VOD'), isTrue);\n      expect(content.contains('#EXT-X-ENDLIST'), isTrue);\n    });\n\n    // ── Apple-style: VOD tag present, no ENDLIST ─────────────────────────────\n    test('Test 3: Apple media playlist (PLAYLIST-TYPE:VOD, no ENDLIST) -> isVod=true', () {\n      const content = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-TARGETDURATION:8\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:7.975,\nfileSequence0.mp4\n#EXTINF:7.941,\nfileSequence1.mp4\n#EXTINF:7.975,\nfileSequence2.mp4\n''';\n      const baseUrl =\n          'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/v5/prog_index.m3u8';\n\n      final playlist = M3u8Parser.parseMediaPlaylist(content, baseUrl);\n\n      expect(content.contains('#EXT-X-PLAYLIST-TYPE:VOD'), isTrue);\n      expect(content.contains('#EXT-X-ENDLIST'), isFalse);\n      expect(playlist.isVod, isTrue);\n      expect(playlist.segments.length, 3);\n    });\n\n    // ── No VOD tag, no ENDLIST ───────────────────────────────────────────────\n    test('Test 4: no VOD tag no ENDLIST -> fallback isVod=true', () {\n      const content = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:10.0,\nhttps://example.com/seg_00000.ts\n#EXTINF:10.0,\nhttps://example.com/seg_00001.ts\n#EXTINF:8.5,\nhttps://example.com/seg_00002.ts\n''';\n      final playlist =\n          M3u8Parser.parseMediaPlaylist(content, 'https://example.com/playlist.m3u8');\n\n      expect(playlist.isVod, isTrue);\n      expect(playlist.segments.length, 3);\n    });\n\n    // ── EVENT playlist without ENDLIST → not VOD ────────────────────────────\n    test('Test 5: PLAYLIST-TYPE:EVENT no ENDLIST -> isVod=false', () {\n      const content = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:10.0,\nhttps://example.com/seg_00000.ts\n#EXTINF:10.0,\nhttps://example.com/seg_00001.ts\n''';\n      final playlist =\n          M3u8Parser.parseMediaPlaylist(content, 'https://example.com/event.m3u8');\n\n      expect(playlist.isVod, isFalse);\n    });\n\n    // ── Explicit VOD tag, no ENDLIST ─────────────────────────────────────────\n    test('Test 6: explicit PLAYLIST-TYPE:VOD no ENDLIST -> isVod=true', () {\n      const content = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:10.0,\nhttps://example.com/seg_00000.ts\n#EXTINF:10.0,\nhttps://example.com/seg_00001.ts\n''';\n      final playlist =\n          M3u8Parser.parseMediaPlaylist(content, 'https://example.com/vod.m3u8');\n      expect(playlist.isVod, isTrue);\n\n      const emptyVod = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-TARGETDURATION:10\n''';\n      final emptyPlaylist =\n          M3u8Parser.parseMediaPlaylist(emptyVod, 'https://example.com/empty.m3u8');\n      expect(emptyPlaylist.isVod, isTrue);\n      expect(emptyPlaylist.segments.length, 0);\n    });\n\n    // ── Nested M3U8 expansion ────────────────────────────────────────────────\n    test('Test 7: nested M3U8 segments are fully resolved', () async {\n      const outerContent = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:30\n#EXTINF:10.0,\nhttps://example.com/seg_00000.ts\n#EXTINF:30.0,\nhttps://example.com/nested.m3u8\n#EXTINF:10.0,\nhttps://example.com/seg_00002.ts\n#EXT-X-ENDLIST\n''';\n\n      const nestedContent = '''\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXTINF:10.0,\nseg_a.ts\n#EXTINF:10.0,\nseg_b.ts\n#EXTINF:10.0,\nseg_c.ts\n#EXT-X-ENDLIST\n''';\n\n      final outer =\n          M3u8Parser.parseMediaPlaylist(outerContent, 'https://example.com/main.m3u8');\n      expect(outer.segments.length, 3);\n      expect(outer.segments.where((s) => s.uri.endsWith('.m3u8')).length, 1);\n\n      final resolved = await M3u8Parser.resolveNestedSegments(\n        outer.segments,\n        (url) async {\n          if (url.contains('nested.m3u8')) return nestedContent;\n          throw Exception('Unknown URL: $url');\n        },\n      );\n\n      expect(resolved.length, 5);\n      expect(resolved.any((s) => s.uri.endsWith('.m3u8')), isFalse);\n      expect(resolved[0].uri, 'https://example.com/seg_00000.ts');\n      expect(resolved[1].uri, 'https://example.com/seg_a.ts');\n      expect(resolved[2].uri, 'https://example.com/seg_b.ts');\n      expect(resolved[3].uri, 'https://example.com/seg_c.ts');\n      expect(resolved[4].uri, 'https://example.com/seg_00002.ts');\n    });\n  });\n}\n"
  },
  {
    "path": "test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility in the flutter_test package. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter_test/flutter_test.dart';\n\n\nvoid main() {\n  testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n  });\n}\n"
  },
  {
    "path": "web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"kazumi\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>kazumi</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n\n  <script>\n    // The value below is injected by flutter build, do not touch.\n    const serviceWorkerVersion = null;\n  </script>\n  <!-- This script adds the flutter initialization JS code -->\n  <script src=\"flutter.js\" defer></script>\n</head>\n<body>\n  <script>\n    window.addEventListener('load', function(ev) {\n      // Download main.dart.js\n      _flutter.loader.loadEntrypoint({\n        serviceWorker: {\n          serviceWorkerVersion: serviceWorkerVersion,\n        },\n        onEntrypointLoaded: function(engineInitializer) {\n          engineInitializer.initializeEngine().then(function(appRunner) {\n            appRunner.runApp();\n          });\n        }\n      });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "web/manifest.json",
    "content": "{\n    \"name\": \"kazumi\",\n    \"short_name\": \"kazumi\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ]\n}\n"
  },
  {
    "path": "windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(kazumi LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"kazumi\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(VERSION 3.14...3.25)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/windows/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <connectivity_plus/connectivity_plus_windows_plugin.h>\n#include <dynamic_color/dynamic_color_plugin_c_api.h>\n#include <flutter_volume_controller/flutter_volume_controller_plugin_c_api.h>\n#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>\n#include <media_kit_video/media_kit_video_plugin_c_api.h>\n#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>\n#include <tray_manager/tray_manager_plugin.h>\n#include <url_launcher_windows/url_launcher_windows.h>\n#include <webview_windows/webview_windows_plugin.h>\n#include <window_manager/window_manager_plugin.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  ConnectivityPlusWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ConnectivityPlusWindowsPlugin\"));\n  DynamicColorPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"DynamicColorPluginCApi\"));\n  FlutterVolumeControllerPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"FlutterVolumeControllerPluginCApi\"));\n  MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"MediaKitLibsWindowsVideoPluginCApi\"));\n  MediaKitVideoPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"MediaKitVideoPluginCApi\"));\n  ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ScreenRetrieverWindowsPluginCApi\"));\n  TrayManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"TrayManagerPlugin\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n  WebviewWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"WebviewWindowsPlugin\"));\n  WindowManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"WindowManagerPlugin\"));\n}\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter/plugin_registry.h>\n\n// Registers Flutter plugins.\nvoid RegisterPlugins(flutter::PluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  connectivity_plus\n  dynamic_color\n  flutter_volume_controller\n  media_kit_libs_windows_video\n  media_kit_video\n  screen_retriever_windows\n  tray_manager\n  url_launcher_windows\n  webview_windows\n  window_manager\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"external_player_utils.cpp\"\n  \"fullscreen_utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Remove the .exp and .lib files generated by the linker.\n# These files are not needed for the application and can be safely removed.\nadd_custom_command(TARGET ${BINARY_NAME}\n  POST_BUILD\n  COMMAND ${CMAKE_COMMAND} -E remove \"$<TARGET_FILE_DIR:${BINARY_NAME}>/${BINARY_NAME}.exp\"\n  COMMAND ${CMAKE_COMMAND} -E remove \"$<TARGET_FILE_DIR:${BINARY_NAME}>/${BINARY_NAME}.lib\"\n)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"kazumi\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"kazumi\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2024 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"kazumi.exe\" \"\\0\"\n            VALUE \"ProductName\", \"kazumi\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "windows/runner/external_player_utils.cpp",
    "content": "// This file is a part of Kazumi\n// (https://github.com/Predidit/Kazumi).\n//\n// Copyright © 2024 Predidit\n// All rights reserved.\n// Use of this source code is governed by GPLv3 license that can be found in the\n// LICENSE file.\n\n#include <windows.h>\n#include <cstdio>\n#include <string>\n#include <iostream>\n#include <fstream>\n#include <random>\n#include \"external_player_utils.h\"\n\nvoid ExternalPlayerUtils::OpenWithPlayer(const char* url) {\n    // temp file path\n    wchar_t tempPath[MAX_PATH];\n    GetTempPathW(MAX_PATH, tempPath);\n\n    // Generate a random file name\n    std::wstring randomFileName = L\"kazumi_stream_\";\n    std::random_device rd;\n    std::mt19937 eng(rd());\n    std::uniform_int_distribution<> distr(10000000, 99999999);\n\n    randomFileName += std::to_wstring(distr(eng)) + L\".m3u8\";\n\n    wchar_t tempFile[MAX_PATH];\n    wcscpy_s(tempFile, tempPath);\n    wcscat_s(tempFile, randomFileName.c_str());\n\n    // write the URL to the temp file\n    std::wofstream outFile(tempFile);\n    if (outFile.is_open()) {\n        outFile << L\"#EXTM3U\\n\";\n        outFile << std::wstring(url, url + strlen(url));\n        outFile.close();\n    } else {\n        return;\n    }\n\n    SHELLEXECUTEINFO execInfo = {0};\n    execInfo.cbSize = sizeof(SHELLEXECUTEINFO);\n    execInfo.fMask = SEE_MASK_INVOKEIDLIST;\n    execInfo.lpVerb = L\"openas\";\n    execInfo.lpFile = tempFile;\n    execInfo.nShow = SW_SHOWNORMAL;\n\n    ShellExecuteEx(&execInfo);\n\n    // DeleteFileW(tempFile);\n}"
  },
  {
    "path": "windows/runner/external_player_utils.h",
    "content": "// This file is a part of Kazumi\n// (https://github.com/Predidit/Kazumi).\n//\n// Copyright © 2024 Predidit\n// All rights reserved.\n// Use of this source code is governed by GPLv3 license that can be found in the\n// LICENSE file.\n\n#ifndef EXTERNAL_PLAYER_UTILS_H_\n#define EXTERNAL_PLAYER_UTILS_H_\n\n#include <cstdint>\n\n#include <Windows.h>\n\nclass ExternalPlayerUtils {\n public:\n  static void OpenWithPlayer(const char* url);\n};\n\n#endif  // EXTERNAL_PLAYER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n#include \"fullscreen_utils.h\"\n#include \"external_player_utils.h\"\n\n#include <optional>\n#include <flutter/method_channel.h>\n#include <flutter/standard_method_codec.h>\n#include <flutter/plugin_registrar_windows.h>\n#include <windows.h>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  // Removed automatic window show to let window_manager plugin control visibility\n  // This prevents window flashing during startup\n  // flutter_controller_->engine()->SetNextFrameCallback([&]() {\n  //   this->Show();\n  // });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  // Register Intent MethodChannel\n  RegisterIntentChannel();\n\n  // Register Storage MethodChannel\n  RegisterStorageChannel();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n\n// Intent MethodChannel setup\nvoid FlutterWindow::RegisterIntentChannel() {\n  auto window_channel =\n      std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(\n          flutter_controller_->engine()->messenger(), \"com.predidit.kazumi/intent\",\n          &flutter::StandardMethodCodec::GetInstance());\n\n  window_channel->SetMethodCallHandler([this](const auto& call, auto result) {\n    if (call.method_name().compare(\"enterFullscreen\") == 0) {\n      FullscreenUtils::EnterNativeFullscreen(GetHandle());\n      result->Success();\n    } else if (call.method_name().compare(\"exitFullscreen\") == 0) {\n      FullscreenUtils::ExitNativeFullscreen(GetHandle());\n      result->Success();\n    } else if (call.method_name().compare(\"openWithMime\") == 0) {\n      const auto* arguments = std::get_if<flutter::EncodableMap>(call.arguments());\n      if (arguments) {\n        auto url_it = arguments->find(flutter::EncodableValue(\"url\"));\n        if (url_it != arguments->end()) {\n          const std::string& url = std::get<std::string>(url_it->second);\n          ExternalPlayerUtils::OpenWithPlayer(url.c_str());\n          result->Success();\n        } else {\n          result->Error(\"InvalidArguments\", \"Missing 'url' argument\");\n        }\n      } else {\n        result->Error(\"InvalidArguments\", \"Arguments are not a map\");\n      }\n    } else {\n      result->NotImplemented();\n    }\n  });\n}\n\n// Storage MethodChannel setup\nvoid FlutterWindow::RegisterStorageChannel() {\n  auto storage_channel =\n      std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(\n          flutter_controller_->engine()->messenger(), \"com.predidit.kazumi/storage\",\n          &flutter::StandardMethodCodec::GetInstance());\n\n  storage_channel->SetMethodCallHandler([](const auto& call, auto result) {\n    if (call.method_name().compare(\"getAvailableStorage\") == 0) {\n      std::wstring path = L\"C:\\\\\";\n      const auto* arguments = std::get_if<flutter::EncodableMap>(call.arguments());\n      if (arguments) {\n        auto path_it = arguments->find(flutter::EncodableValue(\"path\"));\n        if (path_it != arguments->end()) {\n          const std::string& path_str = std::get<std::string>(path_it->second);\n          // Extract drive root from path (e.g. \"C:\\Users\\...\" -> \"C:\\\")\n          if (path_str.length() >= 2 && path_str[1] == ':') {\n            path = std::wstring(1, static_cast<wchar_t>(path_str[0])) + L\":\\\\\";\n          }\n        }\n      }\n\n      ULARGE_INTEGER free_bytes_available;\n      if (GetDiskFreeSpaceExW(path.c_str(), &free_bytes_available, nullptr, nullptr)) {\n        result->Success(flutter::EncodableValue(static_cast<int64_t>(free_bytes_available.QuadPart)));\n      } else {\n        result->Success(flutter::EncodableValue(static_cast<int64_t>(-1)));\n      }\n    } else {\n      result->NotImplemented();\n    }\n  });\n}\n"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n\n  // Register Intent MethodChannel\n  void RegisterIntentChannel();\n\n  // Register Storage MethodChannel\n  void RegisterStorageChannel();\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "windows/runner/fullscreen_utils.cpp",
    "content": "// This file is a part of media_kit\n// (https://github.com/media-kit/media-kit).\n//\n// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.\n// All rights reserved.\n// Use of this source code is governed by MIT license that can be found in the\n// LICENSE file.\n\n#include \"fullscreen_utils.h\"\n\nvoid FullscreenUtils::EnterNativeFullscreen(HWND window) {\n  if (fullscreen_) {\n    return;\n  }\n  fullscreen_ = true;\n\n  // The primary idea here is to revolve around |WS_OVERLAPPEDWINDOW| &\n  // detect/set fullscreen based on it. In the window procedure, this is\n  // separately handled. If there is no |WS_OVERLAPPEDWINDOW| style on the\n  // window i.e. in fullscreen, then no area is left for |WM_NCHITTEST|,\n  // accordingly client area is also expanded to fill whole monitor using\n  // |WM_NCCALCSIZE|.\n\n  auto style = ::GetWindowLongPtr(window, GWL_STYLE);\n  if (style & WS_OVERLAPPEDWINDOW) {\n    auto monitor = MONITORINFO{};\n    auto placement = WINDOWPLACEMENT{};\n    monitor.cbSize = sizeof(MONITORINFO);\n    placement.length = sizeof(WINDOWPLACEMENT);\n    ::GetWindowPlacement(window, &placement);\n    rect_before_fullscreen_ = RECT{\n        placement.rcNormalPosition.left,\n        placement.rcNormalPosition.top,\n        placement.rcNormalPosition.right,\n        placement.rcNormalPosition.bottom,\n    };\n    ::GetMonitorInfo(::MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST),\n                     &monitor);\n    ::SetWindowLongPtr(window, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);\n    ::SetWindowPos(window, HWND_TOP, monitor.rcMonitor.left,\n                   monitor.rcMonitor.top, monitor.rcMonitor.right - monitor.rcMonitor.left,\n                   monitor.rcMonitor.bottom - monitor.rcMonitor.top,\n                   SWP_NOOWNERZORDER | SWP_FRAMECHANGED);\n  }\n}\n\nvoid FullscreenUtils::ExitNativeFullscreen(HWND window) {\n  if (!fullscreen_) {\n    return;\n  }\n  fullscreen_ = false;\n\n  auto style = ::GetWindowLongPtr(window, GWL_STYLE);\n  if (!(style & WS_OVERLAPPEDWINDOW)) {\n    ::SetWindowLongPtr(window, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);\n    if (::IsZoomed(window)) {\n      // Refresh the parent window.\n      ::SetWindowPos(window, nullptr, 0, 0, 0, 0,\n                     SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |\n                         SWP_FRAMECHANGED);\n      auto rect = RECT{};\n      ::GetClientRect(window, &rect);\n      auto flutter_view =\n          ::FindWindowEx(window, nullptr, kFlutterViewWindowClassName, nullptr);\n      ::SetWindowPos(flutter_view, nullptr, rect.left, rect.top,\n                     rect.right - rect.left, rect.bottom - rect.top,\n                     SWP_NOACTIVATE | SWP_NOZORDER);\n    } else {\n      ::SetWindowPos(\n          window, nullptr, rect_before_fullscreen_.left,\n          rect_before_fullscreen_.top,\n          rect_before_fullscreen_.right - rect_before_fullscreen_.left,\n          rect_before_fullscreen_.bottom - rect_before_fullscreen_.top,\n          SWP_NOACTIVATE | SWP_NOZORDER);\n    }\n  }\n}\n\nbool FullscreenUtils::fullscreen_ = false;\n\nRECT FullscreenUtils::rect_before_fullscreen_ = RECT{};\n"
  },
  {
    "path": "windows/runner/fullscreen_utils.h",
    "content": "// This file is a part of media_kit\n// (https://github.com/media-kit/media-kit).\n//\n// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.\n// All rights reserved.\n// Use of this source code is governed by MIT license that can be found in the\n// LICENSE file.\n\n#ifndef FULLSCREEN_UTILS_H_\n#define FULLSCREEN_UTILS_H_\n\n#include <cstdint>\n\n#include <Windows.h>\n\nclass FullscreenUtils {\n public:\n  static void EnterNativeFullscreen(HWND window);\n\n  static void ExitNativeFullscreen(HWND window);\n\n private:\n  static constexpr auto kFlutterViewWindowClassName = L\"FLUTTERVIEW\";\n\n  static bool fullscreen_;\n  static RECT rect_before_fullscreen_;\n};\n\n#endif  // FULLSCREEN_UTILS_H_\n"
  },
  {
    "path": "windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\n// recommended by NVIDIA to enable high-performance GPU\nextern \"C\"\n{\n  __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;\n}\n\n// recommended by AMD to enable high-performance GPU\nextern \"C\"\n{\n  __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;\n}\n\nHANDLE mutex = NULL;\n\n// Window class name must match the one in win32_window.cpp\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\nbool ActivateExistingWindow()\n{\n  // Find the existing window by class name\n  HWND hwnd = ::FindWindow(kWindowClassName, L\"kazumi\");\n  if (hwnd != NULL)\n  {\n    // Check if window is hidden (e.g., minimized to tray)\n    if (!::IsWindowVisible(hwnd))\n    {\n      // Show the hidden window\n      ::ShowWindow(hwnd, SW_SHOW);\n    }\n    // If window is minimized, restore it\n    else if (::IsIconic(hwnd))\n    {\n      ::ShowWindow(hwnd, SW_RESTORE);\n    }\n\n    // Bring window to foreground\n    ::SetForegroundWindow(hwnd);\n\n    // Flash the window to get user's attention\n    FLASHWINFO fwi = {0};\n    fwi.cbSize = sizeof(FLASHWINFO);\n    fwi.hwnd = hwnd;\n    fwi.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG;\n    fwi.uCount = 3;\n    fwi.dwTimeout = 0;\n    ::FlashWindowEx(&fwi);\n\n    return true;\n  }\n  return false;\n}\n\nbool isSingleInstance()\n{\n  if (mutex != NULL)\n  {\n    return true;\n  }\n  std::wstring mutex_str = L\"kazumi.win.mutex\";\n  mutex = ::CreateMutex(NULL, TRUE, mutex_str.c_str());\n  if (mutex == NULL || GetLastError() == ERROR_ALREADY_EXISTS)\n  {\n    CloseHandle(mutex);\n    mutex = NULL;\n    return false;\n  }\n  return true;\n}\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command)\n{\n  // Make sure the application is a single instance.\n  // This is important for the application to work correctly with the local storage.\n  if (!isSingleInstance())\n  {\n    // Try to activate the existing window instead of showing an error\n    ActivateExistingWindow();\n    return EXIT_SUCCESS;\n  }\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())\n  {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  // Disable thread merge to improve performance\n  // Attention: This may impact plugin performance and may be incompatible with future Flutter releases.\n  project.set_ui_thread_policy(flutter::UIThreadPolicy::RunOnSeparateThread);\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.Create(L\"kazumi\", origin, size))\n  {\n    if (mutex) {\n      CloseHandle(mutex);\n    }\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0))\n  {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  if (mutex) {\n    CloseHandle(mutex);\n  }\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr)\n    -1; // remove the trailing null character\n  int input_length = (int)wcslen(utf16_string);\n  std::string utf8_string;\n  if (target_length <= 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      input_length, utf8_string.data(), target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (LOWORD(wparam) == WA_ACTIVE || LOWORD(wparam) == WA_CLICKACTIVE) {\n        if (child_content_ != nullptr) {\n          SetFocus(child_content_);\n        }\n      }\n      return 0;\n\n    case WM_SETFOCUS:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "path": "windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]