Repository: Predidit/Kazumi Branch: main Commit: 177e3c9894ea Files: 339 Total size: 1.9 MB Directory structure: gitextract_7r81mo3e/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ └── other.yml │ └── workflows/ │ ├── pr.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── .metadata ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── kazumi/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ └── values-night/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── assets/ │ ├── bbcode/ │ │ └── BBCode.g4 │ ├── linux/ │ │ ├── DEBIAN/ │ │ │ ├── postinst │ │ │ └── postrm │ │ └── io.github.Predidit.Kazumi.desktop │ ├── plugins/ │ │ ├── 7sefun.json │ │ ├── AGE.json │ │ └── DM84.json │ ├── shaders/ │ │ ├── Anime4K_AutoDownscalePre_x2.glsl │ │ ├── Anime4K_AutoDownscalePre_x4.glsl │ │ ├── Anime4K_Clamp_Highlights.glsl │ │ ├── Anime4K_Restore_CNN_M.glsl │ │ ├── Anime4K_Restore_CNN_S.glsl │ │ ├── Anime4K_Restore_CNN_VL.glsl │ │ ├── Anime4K_Upscale_CNN_x2_M.glsl │ │ ├── Anime4K_Upscale_CNN_x2_S.glsl │ │ ├── Anime4K_Upscale_CNN_x2_VL.glsl │ │ └── LICENSE │ └── statements/ │ └── statements.txt ├── devtools_options.yaml ├── fastlane/ │ └── metadata/ │ └── android/ │ ├── en-US/ │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ └── zh-CN/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchBackground.imageset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── RunnerTests/ │ └── RunnerTests.swift ├── lib/ │ ├── app_module.dart │ ├── app_widget.dart │ ├── bbcode/ │ │ ├── README.md │ │ ├── bbcode_base_listener.dart │ │ ├── bbcode_elements.dart │ │ ├── bbcode_widget.dart │ │ └── generated/ │ │ ├── BBCode.tokens │ │ ├── BBCodeLexer.dart │ │ ├── BBCodeListener.dart │ │ └── BBCodeParser.dart │ ├── bean/ │ │ ├── appbar/ │ │ │ ├── drag_to_move_bar.dart │ │ │ ├── safe_mediaquery_warpper.dart │ │ │ └── sys_app_bar.dart │ │ ├── card/ │ │ │ ├── bangumi_card.dart │ │ │ ├── bangumi_history_card.dart │ │ │ ├── bangumi_info_card.dart │ │ │ ├── bangumi_timeline_card.dart │ │ │ ├── character_card.dart │ │ │ ├── character_comments_card.dart │ │ │ ├── comments_card.dart │ │ │ ├── episode_comments_card.dart │ │ │ ├── network_img_layer.dart │ │ │ ├── palette_card.dart │ │ │ └── staff_card.dart │ │ ├── dialog/ │ │ │ └── dialog_helper.dart │ │ ├── settings/ │ │ │ ├── color_type.dart │ │ │ └── theme_provider.dart │ │ └── widget/ │ │ ├── collect_button.dart │ │ ├── custom_dropdown_menu.dart │ │ ├── embedded_native_control_area.dart │ │ ├── error_widget.dart │ │ └── scrollable_wrapper.dart │ ├── hive_registrar.g.dart │ ├── main.dart │ ├── modules/ │ │ ├── bangumi/ │ │ │ ├── bangumi_item.dart │ │ │ ├── bangumi_item.g.dart │ │ │ ├── bangumi_tag.dart │ │ │ ├── bangumi_tag.g.dart │ │ │ ├── episode_item.dart │ │ │ └── weekday_item.dart │ │ ├── character/ │ │ │ └── character_full_item.dart │ │ ├── characters/ │ │ │ ├── actor_item.dart │ │ │ ├── character_item.dart │ │ │ └── characters_response.dart │ │ ├── collect/ │ │ │ ├── collect_change_module.dart │ │ │ ├── collect_change_module.g.dart │ │ │ ├── collect_module.dart │ │ │ ├── collect_module.g.dart │ │ │ └── collect_type.dart │ │ ├── comments/ │ │ │ ├── comment_item.dart │ │ │ └── comment_response.dart │ │ ├── danmaku/ │ │ │ ├── danmaku_episode_response.dart │ │ │ ├── danmaku_module.dart │ │ │ └── danmaku_search_response.dart │ │ ├── download/ │ │ │ ├── download_module.dart │ │ │ └── download_module.g.dart │ │ ├── history/ │ │ │ ├── history_module.dart │ │ │ └── history_module.g.dart │ │ ├── plugin/ │ │ │ └── plugin_http_module.dart │ │ ├── roads/ │ │ │ └── road_module.dart │ │ ├── search/ │ │ │ ├── plugin_search_module.dart │ │ │ ├── search_history_module.dart │ │ │ └── search_history_module.g.dart │ │ └── staff/ │ │ ├── staff_item.dart │ │ └── staff_response.dart │ ├── pages/ │ │ ├── about/ │ │ │ ├── about_module.dart │ │ │ └── about_page.dart │ │ ├── collect/ │ │ │ ├── collect_controller.dart │ │ │ ├── collect_controller.g.dart │ │ │ ├── collect_module.dart │ │ │ └── collect_page.dart │ │ ├── download/ │ │ │ ├── download_controller.dart │ │ │ ├── download_controller.g.dart │ │ │ ├── download_episode_sheet.dart │ │ │ ├── download_page.dart │ │ │ └── download_page_module.dart │ │ ├── error/ │ │ │ └── storage_error_page.dart │ │ ├── history/ │ │ │ ├── history_controller.dart │ │ │ ├── history_controller.g.dart │ │ │ ├── history_module.dart │ │ │ └── history_page.dart │ │ ├── index_module.dart │ │ ├── index_page.dart │ │ ├── info/ │ │ │ ├── character_page.dart │ │ │ ├── info_controller.dart │ │ │ ├── info_controller.g.dart │ │ │ ├── info_module.dart │ │ │ ├── info_page.dart │ │ │ ├── info_tabview.dart │ │ │ └── source_sheet.dart │ │ ├── init_page.dart │ │ ├── logs/ │ │ │ └── logs_page.dart │ │ ├── menu/ │ │ │ └── menu.dart │ │ ├── my/ │ │ │ ├── my_controller.dart │ │ │ ├── my_controller.g.dart │ │ │ ├── my_module.dart │ │ │ └── my_page.dart │ │ ├── player/ │ │ │ ├── episode_comments_sheet.dart │ │ │ ├── player_controller.dart │ │ │ ├── player_controller.g.dart │ │ │ ├── player_item.dart │ │ │ ├── player_item_panel.dart │ │ │ ├── player_item_surface.dart │ │ │ └── smallest_player_item_panel.dart │ │ ├── plugin_editor/ │ │ │ ├── plugin_editor_page.dart │ │ │ ├── plugin_module.dart │ │ │ ├── plugin_shop_page.dart │ │ │ ├── plugin_test_page.dart │ │ │ └── plugin_view_page.dart │ │ ├── popular/ │ │ │ ├── popular_controller.dart │ │ │ ├── popular_controller.g.dart │ │ │ ├── popular_module.dart │ │ │ └── popular_page.dart │ │ ├── router.dart │ │ ├── search/ │ │ │ ├── search_controller.dart │ │ │ ├── search_controller.g.dart │ │ │ ├── search_module.dart │ │ │ └── search_page.dart │ │ ├── settings/ │ │ │ ├── danmaku/ │ │ │ │ ├── danmaku_module.dart │ │ │ │ ├── danmaku_settings.dart │ │ │ │ ├── danmaku_settings_sheet.dart │ │ │ │ └── danmaku_shield_settings.dart │ │ │ ├── decoder_settings.dart │ │ │ ├── displaymode_settings.dart │ │ │ ├── download_settings.dart │ │ │ ├── interface_settings.dart │ │ │ ├── keyboard_settings.dart │ │ │ ├── player_settings.dart │ │ │ ├── proxy/ │ │ │ │ ├── proxy_editor_page.dart │ │ │ │ ├── proxy_module.dart │ │ │ │ └── proxy_settings_page.dart │ │ │ ├── renderer_settings.dart │ │ │ ├── settings_module.dart │ │ │ ├── super_resolution_settings.dart │ │ │ └── theme_settings_page.dart │ │ ├── timeline/ │ │ │ ├── timeline_controller.dart │ │ │ ├── timeline_controller.g.dart │ │ │ ├── timeline_module.dart │ │ │ └── timeline_page.dart │ │ ├── video/ │ │ │ ├── video_controller.dart │ │ │ ├── video_controller.g.dart │ │ │ ├── video_module.dart │ │ │ └── video_page.dart │ │ └── webdav_editor/ │ │ ├── webdav_editor_page.dart │ │ ├── webdav_module.dart │ │ └── webdav_setting.dart │ ├── plugins/ │ │ ├── anti_crawler_config.dart │ │ ├── plugin_cookie_manager.dart │ │ ├── plugin_install_time_tracker.dart │ │ ├── plugin_validity_tracker.dart │ │ ├── plugins.dart │ │ ├── plugins_controller.dart │ │ └── plugins_controller.g.dart │ ├── providers/ │ │ ├── captcha/ │ │ │ └── captcha_provider.dart │ │ └── video/ │ │ ├── providers.dart │ │ ├── video_source_provider.dart │ │ └── webview_video_source_provider.dart │ ├── repositories/ │ │ ├── collect_crud_repository.dart │ │ ├── collect_repository.dart │ │ ├── download_repository.dart │ │ ├── history_repository.dart │ │ └── search_history_repository.dart │ ├── request/ │ │ ├── api.dart │ │ ├── bangumi.dart │ │ ├── damaku.dart │ │ ├── interceptor.dart │ │ ├── plugin.dart │ │ ├── query_manager.dart │ │ └── request.dart │ ├── shaders/ │ │ ├── shaders_controller.dart │ │ └── shaders_controller.g.dart │ ├── utils/ │ │ ├── anime_season.dart │ │ ├── auto_updater.dart │ │ ├── background_download_service.dart │ │ ├── constants.dart │ │ ├── download_manager.dart │ │ ├── extension.dart │ │ ├── external_player.dart │ │ ├── format_utils.dart │ │ ├── logger.dart │ │ ├── m3u8_ad_filter.dart │ │ ├── m3u8_parser.dart │ │ ├── mortis.dart │ │ ├── proxy_manager.dart │ │ ├── proxy_utils.dart │ │ ├── remote.dart │ │ ├── search_parser.dart │ │ ├── storage.dart │ │ ├── string_match.dart │ │ ├── syncplay.dart │ │ ├── syncplay_endpoint.dart │ │ ├── timed_shutdown_service.dart │ │ ├── utils.dart │ │ └── webdav.dart │ └── webview/ │ ├── captcha/ │ │ ├── captcha_webview_controller.dart │ │ └── impl/ │ │ ├── captcha_webview_inappwebview_impl.dart │ │ ├── captcha_webview_linux_impl.dart │ │ └── captcha_webview_windows_impl.dart │ └── video/ │ ├── impl/ │ │ ├── video_webview_android_impl.dart │ │ ├── video_webview_apple_impl.dart │ │ ├── video_webview_impl.dart │ │ ├── video_webview_linux_impl.dart │ │ └── video_webview_windows_impl.dart │ └── video_webview_controller.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ ├── Release.entitlements │ │ ├── en-GB.lproj/ │ │ │ └── MainMenu.strings │ │ ├── en.lproj/ │ │ │ └── MainMenu.strings │ │ └── zh-Hans.lproj/ │ │ └── MainMenu.strings │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── RunnerTests/ │ └── RunnerTests.swift ├── pubspec.yaml ├── test/ │ ├── m3u8_parser_test.dart │ └── widget_test.dart ├── web/ │ ├── index.html │ └── manifest.json └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── external_player_utils.cpp ├── external_player_utils.h ├── flutter_window.cpp ├── flutter_window.h ├── fullscreen_utils.cpp ├── fullscreen_utils.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Set default behavior to automatically normalize line endings * text=auto # Explicitly declare text files you want to always be normalized and converted to native line endings on checkout *.dart text eol=lf *.yaml text eol=lf *.yml text eol=lf *.json text eol=lf *.xml text eol=lf *.html text eol=lf *.css text eol=lf *.js text eol=lf *.md text eol=lf *.txt text eol=lf *.sh text eol=lf *.gradle text eol=lf *.properties text eol=lf # Source code files (C/C++/Objective-C/Swift) *.c text eol=lf *.cc text eol=lf *.cpp text eol=lf *.h text eol=lf *.hpp text eol=lf *.m text eol=lf *.mm text eol=lf *.swift text eol=lf # CMake files *.cmake text eol=lf CMakeLists.txt text eol=lf # Generated files should always use LF (Flutter generates these) **/generated_plugin_registrant.* text eol=lf **/generated_plugins.cmake text eol=lf **/GeneratedPluginRegistrant.* text eol=lf # Windows-specific files use CRLF *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf # Visual Studio files *.sln text eol=crlf *.vcxproj text eol=crlf *.vcxproj.filters text eol=crlf # Denote all files that are truly binary and should not be modified *.png binary *.jpg binary *.jpeg binary *.gif binary *.ico binary *.ttf binary *.otf binary *.eot binary *.woff binary *.woff2 binary *.so binary *.dylib binary *.dll binary *.exe binary *.jar binary *.aar binary *.apk binary *.aab binary *.zip binary *.tar binary *.gz binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug 反馈 description: 提交一个 Bug 反馈。 title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | 请详细填写以下内容~ - type: textarea id: buginfo attributes: label: 在使用的时候发生了什么 Bug ? description: 并且,还请写出您是如何触发这个 Bug 的。 validations: required: true - type: dropdown id: os attributes: label: 您在使用哪个操作系统? multiple: false options: - Android - Windows - macOS / iOS - Linux validations: required: true - type: textarea id: osver attributes: label: 请具体提供设备、版本号等信息。 description: 例如,“Redmi K40S,Android 13”、“Windows 10 22H2” 等。 validations: required: true - type: textarea id: hardware attributes: label: (选填)一些与 Bug 相关的硬件信息。 description: (选填)例如,有视频播放问题,可以填写“显卡型号”、“显卡驱动版本”等。 - type: textarea id: logs attributes: label: 日志信息 description: 请在 “我的 - 关于 - 错误日志” 界面复制错误日志,并粘贴在这里。 value: |
Log ```shell [在此处粘贴你的日志] ```
validations: required: true - type: checkboxes id: terms attributes: label: 提交前确认 description: 在提交前,请确认以下内容 options: - label: issue 列表中,没有我发现的这个 Bug required: true - label: 我正在使用最新版本的 Kazumi required: true ================================================ FILE: .github/ISSUE_TEMPLATE/other.yml ================================================ name: 其他 issue description: 新功能需求、问题询问等 body: - type: markdown attributes: value: | 请详细填写以下内容~ - type: textarea id: buginfo attributes: label: issue 内容 description: 请填写您的 issue 内容。要添加附件,请点击输入框后,直接将附件拖进输入框。 validations: required: true - type: checkboxes id: terms attributes: label: 提交前确认 description: 在提交前,请确认以下内容 options: - label: issue 列表中,没有我的新功能需求 / 问题 required: true ================================================ FILE: .github/workflows/pr.yaml ================================================ --- name: "PR workflow" on: pull_request: types: - opened - synchronize - reopened - ready_for_review paths-ignore: - 'static/**' - '**.md' - '.gitignore' - '.github/ISSUE_TEMPLATE/**' - 'fastlane/**' workflow_dispatch: inputs: logLevel: description: 'Log level' required: true default: 'warning' signpath_sign: description: 'sign binary by signpath' required: false default: false type: boolean run_android: description: 'manually run android build' required: false default: false type: boolean run_windows: description: 'manually run windows build' required: false default: false type: boolean run_ios: description: 'manually run ios build' required: false default: false type: boolean run_macos: description: 'manually run macos build' required: false default: false type: boolean run_linux: description: 'manually run linux build' required: false default: false type: boolean jobs: changes: if: ${{ ! github.event.pull_request.draft }} runs-on: "ubuntu-latest" permissions: pull-requests: read outputs: android: ${{ steps.filter.outputs.android }} windows: ${{ steps.filter.outputs.windows }} ios: ${{ steps.filter.outputs.ios }} macos: ${{ steps.filter.outputs.macos }} linux: ${{ steps.filter.outputs.linux }} all: ${{ steps.filter.outputs.all }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: predicate-quantifier: 'every' filters: | android: - 'android/**' windows: - 'windows/**' ios: - 'ios/**' macos: - 'macos/**' linux: - 'linux/**' all: - '!android/**' - '!windows/**' - '!ios/**' - '!macos/**' - '!linux/**' flutter-build-android: needs: changes if: ${{ github.event.inputs.run_android || (! github.event.pull_request.draft && (needs.changes.outputs.android || needs.changes.outputs.all)) }} name: "Release for android" runs-on: "ubuntu-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev libasound2-dev shell: bash - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Get Flutter dependencies run: flutter pub get shell: bash - name: Print Flutter version run: flutter doctor -v shell: bash - name: Build Flutter for Android run: flutter build apk --split-per-abi shell: bash - name: Package android build output run: cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk Kazumi_android_canary.apk shell: bash - name: Upload android outputs uses: actions/upload-artifact@v4 with: name: android_outputs path: Kazumi_android_*.apk flutter-build-windows: needs: changes if: ${{ github.event.inputs.run_windows || (! github.event.pull_request.draft && (needs.changes.outputs.windows || needs.changes.outputs.all)) }} name: "Release for windows" runs-on: "windows-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - run: choco install yq - name: Enable Git longpaths run: git config --system core.longpaths true - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Set up Java uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '18' - run: flutter pub get - run: flutter build windows - run: Compress-Archive build/windows/x64/runner/Release/* Kazumi_windows_canary.zip - name: Upload windows outputs id: unsigned-windows-packet-artifacts uses: actions/upload-artifact@v4 with: name: windows_outputs path: | Kazumi_windows_*.zip # - name: Build unsigned msix # run: dart run msix:create # - name: Upload windows msix ouputs # uses: actions/upload-artifact@v4 # id: unsigned-windows-msix-artifacts # with: # name: windows_msix_outputs # path: | # build/windows/x64/runner/Release/kazumi.msix # - run: New-Item -Path "build/windows/msix_signed_output" -ItemType Directory # - name: sign windows msix # if: ${{ github.event.inputs.signpath_sign == 'true'}} # uses: signpath/github-action-submit-signing-request@v1 # with: # api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' # organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877' # project-slug: 'Kazumi' # signing-policy-slug: 'test-signing' # artifact-configuration-slug: 'MSIX' # github-artifact-id: '${{ steps.unsigned-windows-msix-artifacts.outputs.artifact-id }}' # wait-for-completion: true # output-artifact-directory: 'build/windows/msix_signed_output' # - name: Upload windows msix signed ouputs # if: ${{ github.event.inputs.signpath_sign == 'true'}} # uses: actions/upload-artifact@v4 # id: signed-windows-msix-artifacts # with: # name: windows_msix_signed_outputs # path: build/windows/msix_signed_output/*.msix flutter-build-ios: needs: changes if: ${{ github.event.inputs.run_ios || (! github.event.pull_request.draft && (needs.changes.outputs.ios || needs.changes.outputs.all)) }} name: "Release for iOS" runs-on: "macos-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - run: flutter pub get - name: Build IPA run: | flutter build ios --release --no-codesign - name: Create IPA run: | mkdir Payload cp -R build/ios/iphoneos/Runner.app Payload/Runner.app find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \; zip -q -r Kazumi_ios_canary_no_sign.ipa Payload - name: Upload iOS build uses: actions/upload-artifact@v4 with: name: ios_outputs path: Kazumi_ios_*.ipa flutter-build-macos: needs: changes if: ${{ github.event.inputs.run_macos || (! github.event.pull_request.draft && (needs.changes.outputs.macos || needs.changes.outputs.all)) }} name: "Release for Macos" runs-on: "macos-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - run: flutter pub get - run: flutter build macos --release - name: Create DMG run: | npm install --global create-dmg create-dmg build/macos/Build/Products/Release/Kazumi.app continue-on-error: true - name: Rename DMG run: mv Kazumi*.dmg Kazumi_macos_canary.dmg - name: Upload MacOS build uses: actions/upload-artifact@v4 with: name: macos_outputs path: Kazumi_macos_*.dmg flutter-build-linux: needs: changes if: ${{ github.event.inputs.run_linux || (! github.event.pull_request.draft && (needs.changes.outputs.linux || needs.changes.outputs.all)) }} name: "Release for Linux" runs-on: "ubuntu-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \ libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \ libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \ libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \ libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \ libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \ libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \ libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \ libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \ linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \ x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \ unixodbc-dev libpq-dev libxxhash-dev libaom-dev shell: bash - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Get Flutter dependencies run: flutter pub get shell: bash - name: Build Flutter for Linux run: flutter build linux shell: bash # - name: Download FFmpeg Assets # uses: dsaltares/fetch-gh-release-asset@master # with: # repo: 'Predidit/avbuild' # version: 'tags/1.1.0' # file: 'ffmpeg_linux_amd64.zip' # token: ${{ secrets.GITHUB_TOKEN }} # - run: rm -f build/linux/x64/release/bundle/lib/libffmpeg.so.7 # - run: unzip ffmpeg_linux_amd64.zip -d build/linux/x64/release/bundle/lib - name: Package linux build output run: | # Tarball package tar -zcvf Kazumi_linux_canary.tar.gz -C build/linux/x64/release/bundle . # Debian package mkdir Kazumi_linux_canary_amd64 cd Kazumi_linux_canary_amd64 mkdir -p opt/Kazumi mkdir -p usr/share/applications mkdir -p usr/share/icons/hicolor/512x512/apps cp -r ../build/linux/x64/release/bundle/* opt/Kazumi cp -r ../assets/linux/DEBIAN . chmod 0755 DEBIAN/postinst chmod 0755 DEBIAN/postrm cat>DEBIAN/control< Package: Kazumi Version: 0.0.1 Section: x11 Priority: optional Architecture: amd64 Essential: no Installed-Size: 34648 Description: Watch Animes online with danmaku support. Homepage: https://github.com/Predidit/Kazumi Depends: libayatana-appindicator3-1, gir1.2-ayatanaappindicator3-0.1, libwebkit2gtk-4.1-0 EOF cp ../assets/linux/io.github.Predidit.Kazumi.desktop usr/share/applications cp ../assets/images/logo/logo_linux.png usr/share/icons/hicolor/512x512/apps/io.github.Predidit.Kazumi.png cd .. dpkg-deb --build --root-owner-group Kazumi_linux_canary_amd64 shell: bash - name: Upload linux outputs uses: actions/upload-artifact@v4 with: name: linux_outputs path: | Kazumi_linux_*.tar.gz Kazumi_linux_*.deb ================================================ FILE: .github/workflows/release.yaml ================================================ --- name: "release" on: push: tags: - "*" workflow_dispatch: inputs: logLevel: description: 'Log level' required: true default: 'warning' jobs: flutter-build-android: name: "Release for android" runs-on: "ubuntu-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Extract tag name run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV shell: bash - name: Echo build progress run: echo "Kazumi_android_${{ env.tag }}.apk build progress" shell: bash - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev libasound2-dev shell: bash - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Get Flutter dependencies run: flutter pub get shell: bash - name: Inject DanDan API Credentials run: | sed -i "s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g" lib/utils/mortis.dart sed -i "s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g" lib/utils/mortis.dart - name: Build Flutter for Android run: flutter build apk --split-per-abi shell: bash - name: Package android build output run: cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk Kazumi_android_${env:tag}.apk shell: bash - name: Upload android outputs uses: actions/upload-artifact@v4 with: name: android_outputs path: Kazumi_android_*.apk flutter-build-windows: name: "Release for windows" runs-on: "windows-latest" needs: [flutter-build-android, flutter-build-ios, flutter-build-linux, flutter-build-macos] permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - run: | $tag = "${{ github.ref }}".Replace('refs/tags/', '') echo "tag=$(echo $tag)" >> $env:GITHUB_ENV - run: echo "Kazumi_windows_${env:tag}.zip build progress" - run: choco install yq - name: Enable Git longpaths run: git config --system core.longpaths true - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Set up Java uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '18' - run: flutter pub get - name: Inject DanDan API Credentials env: DANDANAPI_APPID: ${{ secrets.DANDANAPI_APPID }} DANDANAPI_KEY: ${{ secrets.DANDANAPI_KEY }} run: | (Get-Content -Path 'lib/utils/mortis.dart') -replace "kvpx7qkqjh", "$env:DANDANAPI_APPID" | Set-Content -Path 'lib/utils/mortis.dart' (Get-Content -Path 'lib/utils/mortis.dart') -replace "rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax", "$env:DANDANAPI_KEY" | Set-Content -Path 'lib/utils/mortis.dart' - run: flutter build windows - run: Compress-Archive build/windows/x64/runner/Release/* Kazumi_windows_${env:tag}.zip - name: Upload windows outputs uses: actions/upload-artifact@v4 id: unsigned-windows-zip-artifacts with: name: windows_outputs path: | Kazumi_windows_*.zip # Sign Zip - run: New-Item -Path "build/windows/zip_signed_output" -ItemType Directory - name: sign windows zip uses: signpath/github-action-submit-signing-request@v1.1 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877' project-slug: 'Kazumi' signing-policy-slug: 'release-signing' artifact-configuration-slug: 'Packet' github-artifact-id: '${{ steps.unsigned-windows-zip-artifacts.outputs.artifact-id }}' wait-for-completion: true output-artifact-directory: 'build/windows/zip_signed_output' - name: Upload windows zip signed ouputs uses: actions/upload-artifact@v4 id: signed-windows-zip-artifacts with: name: windows_zip_signed_outputs path: build/windows/zip_signed_output/*.zip # Replace Unpacked Artifact with Signed Artifact - name: Replace Unpacked Artifact with Signed Artifact run: Expand-Archive -Path "build/windows/zip_signed_output/Kazumi_windows_${env:tag}.zip" -DestinationPath "build/windows/x64/runner/Release" -Force # Build Unsigned MSIX - name: Build unsigned msix run: dart run msix:create - name: Upload windows msix ouputs uses: actions/upload-artifact@v4 id: unsigned-windows-msix-artifacts with: name: windows_msix_outputs path: | build/windows/x64/runner/Release/kazumi.msix # Sign MSIX - run: New-Item -Path "build/windows/msix_signed_output" -ItemType Directory - name: sign windows msix uses: signpath/github-action-submit-signing-request@v1.1 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: 'fa047255-4772-4be1-b14f-5cfa62635877' project-slug: 'Kazumi' signing-policy-slug: 'release-signing' artifact-configuration-slug: 'MSIX' github-artifact-id: '${{ steps.unsigned-windows-msix-artifacts.outputs.artifact-id }}' wait-for-completion: true output-artifact-directory: 'build/windows/msix_signed_output' - name: Upload windows msix signed ouputs uses: actions/upload-artifact@v4 id: signed-windows-msix-artifacts with: name: windows_msix_signed_outputs path: build/windows/msix_signed_output/*.msix flutter-build-linux: name: "Release for Linux" runs-on: "ubuntu-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Extract tag name run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV shell: bash - name: Echo build progress run: echo "Kazumi_linux_${{ env.tag }}.tar.gz build progress" shell: bash - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \ libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \ libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \ libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \ libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \ libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \ libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \ libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \ libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \ linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \ x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \ unixodbc-dev libpq-dev libxxhash-dev libaom-dev shell: bash - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - name: Get Flutter dependencies run: flutter pub get shell: bash - name: Inject DanDan API Credentials run: | sed -i "s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g" lib/utils/mortis.dart sed -i "s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g" lib/utils/mortis.dart - name: Build Flutter for Linux run: flutter build linux shell: bash # - name: Download FFmpeg Assets # uses: dsaltares/fetch-gh-release-asset@master # with: # repo: 'Predidit/avbuild' # version: 'tags/1.1.0' # file: 'ffmpeg_linux_amd64.zip' # token: ${{ secrets.GITHUB_TOKEN }} # - run: rm -f build/linux/x64/release/bundle/lib/libffmpeg.so.7 # - run: unzip ffmpeg_linux_amd64.zip -d build/linux/x64/release/bundle/lib - name: Package linux build output run: | # Tarball package tar -zcvf Kazumi_linux_${{ env.tag }}_amd64.tar.gz -C build/linux/x64/release/bundle . # Debian package mkdir Kazumi_linux_${{ env.tag }}_amd64 cd Kazumi_linux_${{ env.tag }}_amd64 mkdir -p opt/Kazumi mkdir -p usr/share/applications mkdir -p usr/share/icons/hicolor/512x512/apps cp -r ../build/linux/x64/release/bundle/* opt/Kazumi cp -r ../assets/linux/DEBIAN . chmod 0755 DEBIAN/postinst chmod 0755 DEBIAN/postrm cat>DEBIAN/control< Package: Kazumi Version: ${{ env.tag }} Section: x11 Priority: optional Architecture: amd64 Essential: no Installed-Size: 34648 Description: Watch Animes online with danmaku support. Homepage: https://github.com/Predidit/Kazumi Depends: libayatana-appindicator3-1, gir1.2-ayatanaappindicator3-0.1, libwebkit2gtk-4.1-0 EOF cp ../assets/linux/io.github.Predidit.Kazumi.desktop usr/share/applications cp ../assets/images/logo/logo_linux.png usr/share/icons/hicolor/512x512/apps/io.github.Predidit.Kazumi.png cd .. dpkg-deb --build --root-owner-group Kazumi_linux_${{ env.tag }}_amd64 shell: bash - name: Upload linux outputs uses: actions/upload-artifact@v4 with: name: linux_outputs path: | Kazumi_linux_*.tar.gz Kazumi_linux_*.deb flutter-build-ios: name: "Release for iOS" runs-on: "macos-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Extract tag name run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Echo build progress run: echo "Kazumi_ios_${{ env.tag }}.ipa build progress" - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - run: flutter pub get - name: Inject DanDan API Credentials run: | sed -i '' "s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g" lib/utils/mortis.dart sed -i '' "s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g" lib/utils/mortis.dart - name: Build IPA run: | flutter build ios --release --no-codesign - name: Create IPA run: | mkdir Payload cp -R build/ios/iphoneos/Runner.app Payload/Runner.app find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \; zip -q -r Kazumi_ios_${{ env.tag }}_no_sign.ipa Payload - name: Upload iOS build uses: actions/upload-artifact@v4 with: name: ios_outputs path: Kazumi_ios_*.ipa flutter-build-macos: name: "Release for Macos" runs-on: "macos-latest" permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Extract tag name run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Echo build progress run: echo "Kazumi_macos_${{ env.tag }}.dmg build progress" - name: Set up Flutter uses: subosito/flutter-action@v2.16.0 with: channel: stable flutter-version-file: pubspec.yaml - run: flutter pub get - name: Inject DanDan API Credentials run: | sed -i '' "s/kvpx7qkqjh/${{ secrets.DANDANAPI_APPID }}/g" lib/utils/mortis.dart sed -i '' "s/rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax/${{ secrets.DANDANAPI_KEY }}/g" lib/utils/mortis.dart - run: flutter build macos --release - name: Create DMG run: | npm install --global create-dmg create-dmg build/macos/Build/Products/Release/Kazumi.app continue-on-error: true - name: Rename DMG run: mv Kazumi*.dmg Kazumi_macos_${{ env.tag }}.dmg - name: Upload MacOS build uses: actions/upload-artifact@v4 with: name: macos_outputs path: Kazumi_macos_*.dmg release: name: "Release" runs-on: "ubuntu-latest" needs: [flutter-build-windows] permissions: write-all steps: - name: Clone repository uses: actions/checkout@v4 - name: Extract tag name run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV shell: bash - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Setup Android build tools run: sdkmanager "build-tools;34.0.0" - name: Download windows zip build file uses: actions/download-artifact@v4 with: name: windows_zip_signed_outputs path: windows_zip_signed_outputs - name: List files in windows_outputs directory run: ls -l windows_zip_signed_outputs - name: Copy windows build file to root run: cp windows_zip_signed_outputs/* Kazumi_windows_${{ env.tag }}.zip - name: Download windows msix build file uses: actions/download-artifact@v4 with: name: windows_msix_signed_outputs path: windows_msix_signed_outputs - name: List files in windows_msix_signed_outputs directory run: ls -l windows_msix_signed_outputs - name: Copy windows build file to root run: cp windows_msix_signed_outputs/* Kazumi_windows_${{ env.tag }}.msix - name: Download android build file uses: actions/download-artifact@v4 with: name: android_outputs path: android_outputs - name: List files in android_outputs directory run: ls -l android_outputs - name: Copy android build file to unsigned floder run: | mkdir build mkdir build/unsigned mkdir build/signed cp android_outputs/* build/unsigned/Kazumi_android_${{ env.tag }}.apk - name: Download iOS build file uses: actions/download-artifact@v4 with: name: ios_outputs path: ios_outputs - name: List files in ios_outputs directory run: ls -l ios_outputs - name: Copy ios build file to root run: cp ios_outputs/* Kazumi_ios_${{ env.tag }}_no_sign.ipa - name: Download macos build file uses: actions/download-artifact@v4 with: name: macos_outputs path: macos_outputs - name: List files in macos_outputs directory run: ls -l macos_outputs - name: Copy macos build file to root run: cp macos_outputs/* Kazumi_macos_${{ env.tag }}.dmg - name: Download linux build file uses: actions/download-artifact@v4 with: name: linux_outputs path: linux_outputs - name: List files in linux_outputs directory run: ls -l linux_outputs - name: Copy linux build file to root run: cp linux_outputs/* . - name: Sign APK id: sign_app uses: filippoLeporati93/android-release-signer@v1 with: releaseDirectory: build/unsigned signingKeyBase64: ${{ secrets.SIGNING_KEY_BASE64 }} alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} env: BUILD_TOOLS_VERSION: "34.0.0" - name: Copy Signed android build file run: cp ${{steps.sign_app.outputs.signedReleaseFile}} build/signed/Kazumi_android_${{ env.tag }}.apk - name: Create release uses: softprops/action-gh-release@v2 with: files: | build/signed/*.apk Kazumi_windows_*.zip Kazumi_windows_*.msix Kazumi_macos_*.dmg Kazumi_ios_*.ipa Kazumi_linux_*.tar.gz Kazumi_linux_*.deb ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release # Added after flutter 3.29 /android/app/.cxx/ android/build/reports/problems/ # CocoaPods - iOS/macOS dependencies # Podfile.lock is auto-generated by Flutter, no need to commit **/ios/Pods/ **/macos/Pods/ **/ios/Podfile.lock **/macos/Podfile.lock ================================================ FILE: .gitmodules ================================================ [submodule "fastlane/.flutter"] path = fastlane/.flutter url = https://github.com/flutter/flutter.git branch = stable [submodule "fastlane/.libmpv-android-video-build"] path = fastlane/.libmpv-android-video-build url = https://github.com/Predidit/libmpv-android-video-build.git ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: android create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: ios create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: linux create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: macos create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: web create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: windows create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: .vscode/settings.json ================================================ { "cmake.ignoreCMakeListsMissing": true, "search.exclude": { "**/fastlane": true }, "dart.analysisExcludedFolders": [ "**/fastlane/**" ], "files.associations": { "xiosbase": "cpp", "utility": "cpp", "xstring": "cpp", "xtree": "cpp", "algorithm": "cpp", "any": "cpp", "array": "cpp", "atomic": "cpp", "bit": "cpp", "cctype": "cpp", "charconv": "cpp", "chrono": "cpp", "cinttypes": "cpp", "clocale": "cpp", "cmath": "cpp", "codecvt": "cpp", "compare": "cpp", "concepts": "cpp", "condition_variable": "cpp", "coroutine": "cpp", "cstddef": "cpp", "cstdint": "cpp", "cstdio": "cpp", "cstdlib": "cpp", "cstring": "cpp", "ctime": "cpp", "cwchar": "cpp", "exception": "cpp", "filesystem": "cpp", "format": "cpp", "forward_list": "cpp", "functional": "cpp", "future": "cpp", "initializer_list": "cpp", "iomanip": "cpp", "ios": "cpp", "iosfwd": "cpp", "iostream": "cpp", "istream": "cpp", "iterator": "cpp", "limits": "cpp", "list": "cpp", "locale": "cpp", "map": "cpp", "memory": "cpp", "mutex": "cpp", "new": "cpp", "optional": "cpp", "ostream": "cpp", "ratio": "cpp", "set": "cpp", "sstream": "cpp", "stdexcept": "cpp", "stop_token": "cpp", "streambuf": "cpp", "string": "cpp", "system_error": "cpp", "thread": "cpp", "tuple": "cpp", "type_traits": "cpp", "typeinfo": "cpp", "unordered_map": "cpp", "variant": "cpp", "vector": "cpp", "xfacet": "cpp", "xhash": "cpp", "xlocale": "cpp", "xlocbuf": "cpp", "xlocinfo": "cpp", "xlocmes": "cpp", "xlocmon": "cpp", "xlocnum": "cpp", "xloctime": "cpp", "xmemory": "cpp", "xtr1common": "cpp", "xutility": "cpp" } } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Kazumi 使用 Flutter 开发的基于自定义规则的番剧采集与在线观看程序。使用最多五行基于 `Xpath` 语法的选择器构建自己的规则。支持规则导入与规则分享。支持基于 `Anime4K` 的实时超分辨率。绝赞开发中 (~ ̄▽ ̄)~ ## 支持平台 - Android 10 及以上 - Windows 10 及以上 - MacOS 10.15 及以上 - Linux (实验性) - iOS 13 及以上 (需要[自签名](https://kazumi.app/docs/misc/how-to-install-in-ios.html)) - HarmonyOS 5.0 及以上 (位于[分支仓库](https://github.com/ErBWs/Kazumi/releases/latest),需要[侧载](https://kazumi.app/docs/misc/how-to-install-in-ohos.html)) ## 屏幕截图
## 功能 / 开发计划 - [x] 规则编辑器 - [x] 番剧目录 - [x] 番剧搜索 - [x] 番剧时间表 - [x] 番剧字幕 - [x] 分集播放 - [x] 视频播放器 - [x] 多视频源支持 - [x] 规则分享 - [x] 硬件加速 - [x] 高刷适配 - [x] 追番列表 - [x] 番剧弹幕 - [x] 在线更新 - [x] 历史记录 - [x] 倍速播放 - [x] 配色方案 - [x] 跨设备同步 - [x] 无线投屏 (DLNA) - [x] 外部播放器播放 - [x] 超分辨率 - [x] 一起看 - [ ] 番剧下载 - [ ] 番剧更新提醒 - [ ] 还有更多 (/・ω・\) ## 下载 通过本页面 [releases](https://github.com/Predidit/Kazumi/releases) 选项卡下载: Get it on Github ### Android Get it on F-Droid ### GNU/Linux    Get it on Flathub #### Arch Linux 可以从 [AUR](http://aur.archlinux.org) 或 [archlinuxcn](https://github.com/archlinuxcn/repo) 安装。 ##### AUR ```bash [yay/paru] -S kazumi # 从源码构建 [yay/paru] -S kazumi-bin # 二进制包 ``` ##### archlinuxcn ```bash sudo pacman -S kazumi ``` ## 贡献 欢迎向我们的 [规则仓库](https://github.com/Predidit/KazumiRules) 提交您的自定义规则。您可以自由选择是否在规则中留下您的ID ## Q&A
使用者 Q&A #### Q: 为什么少数番剧中有广告? A: 本项目未插入任何广告。广告来自视频源, 请不要相信广告中的任何内容, 并尽量选择没有广告的视频源观看。 #### Q: 为什么我启用超分辨率功能后播放卡顿? A: 超分辨率功能对 GPU 性能要求较高, 如果没有在高性能独立显卡上运行 Kazumi, 尽量选择效率档而非质量档。对低分辨率视频源而非高分辨率视频源使用超分也可以降低性能消耗。 #### Q: 为什么播放视频时内存占用较高? A: 本程序在视频播放时, 会尽可能多地缓存视频到内存, 以提供较好的观看体验。如果您的内存较为紧张, 可以在播放设置选项卡启用低内存模式, 这将限制缓存。 #### Q: 为什么少数番剧无法通过外部播放器观看? A: 部分视频源的番剧使用了反盗链措施, 这可以被 Kazumi 解决, 但无法被外部播放器解决。 #### Q: 为什么下载的 Linux 版本缺少图标和托盘功能? A: 使用 .deb 版本进行安装, tar.gz 版本仅为方便二次打包, 这一格式先天缺乏图标和托盘功能支持。
规则编写者 Q&A #### Q: 为什么我的自定义规则无法实现检索? A: 目前我们对 `Xpath` 语法的支持并不完整, 我们目前只支持以 `//` 开头的选择器。建议参照我们给出的示例规则构建自定义规则。 #### Q: 为什么我的自定义规则可以实现检索, 但不能实现观看? A: 尝试关闭自定义规则的使用内置播放器选项, 这将尝试使用 `webview` 进行播放, 提高兼容性。但在内置播放器可用时, 建议启用内置播放器, 以获得更加流畅并带有弹幕的观看体验。
开发者 Q&A #### Q: 我在尝试自行编译该项目, 但编译没有成功。 A: 本项目编译需要良好的网络环境, 除了由 Google 托管的 Flutter 相关依赖外, 本项目同样依赖托管在 MavenCentral/Github/SourceForge 上的资源。如果您位于中国大陆, 可能需要设置恰当的镜像地址。
## 美术资源 本项目图标来自 [Yuquanaaa](https://www.pixiv.net/users/66219277) 发表在 [Pixiv](https://www.pixiv.net/artworks/116666979) 上的作品。 此图标由其原作者 [Yuquanaaa](https://www.pixiv.net/users/66219277) 拥有版权。我们已获得原作者的授权和许可, 可以在本项目中使用这一图标。这一图标不是自由使用的, 未经原作者明确授权, 任何人不得擅自使用、复制、修改或分发这一图标。 本项目内嵌字体为 [Mi Sans](https://hyperos.mi.com/font/en/details/sc/) 字体, 由 [Xiaomi](https://www.mi.com/) 开发和拥有版权。 ## 免责声明 本项目基于 GNU 通用公共许可证第 3 版(GPL-3.0)授权。我们不对其适用性、可靠性或准确性作出任何明示或暗示的保证。在法律允许的最大范围内, 作者和贡献者不承担任何因使用本软件而产生的直接、间接、偶然、特殊或后果性的损害赔偿责任。 使用本项目需遵守所在地法律法规, 不得进行任何侵犯第三方知识产权的行为。因使用本项目而产生的数据和缓存应在24小时内清除, 超出 24 小时的使用需获得相关权利人的授权。 ## 隐私政策 (Privacy policy) 我们不收集任何用户数据, 不使用任何遥测组件。 ## 代码签名策略 (Code signing policy) 提交者: [Contributors](https://github.com/Predidit/Kazumi/graphs/contributors) 审阅者: [Owner](https://github.com/Predidit) ## 赞助 (Sponsors) | ![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/) | |------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| ## 致谢 特别感谢 [XpathSelector](https://github.com/simonkimi/xpath_selector) 这个优秀的项目是本项目的基石。 特别感谢 [DandanPlayer](https://www.dandanplay.com/) 本项目使用了 dandanplayer 开放 API 以提供弹幕交互。 特别感谢 [Bangumi](https://bangumi.tv/) 本项目使用了 Bangumi 开放 API 以提供番剧元数据。 特别感谢 [Anime4K](https://github.com/bloc97/Anime4K) 本项目使用 Anime4K 进行实时超分。 特别感谢 [SyncPlay](https://github.com/Syncplay/syncplay) 本项目使用 SyncPlay 协议并通过 SyncPlay 公共服务器实现一起看功能。 感谢 [media-kit](https://github.com/media-kit/media-kit) 本项目跨平台媒体播放能力来自 media-kit。 感谢 [avbuild](https://github.com/wang-bin/avbuild) 本项目使用了来自 avbuild 的树外补丁实现非标准视频流播放。 感谢 [hive](https://github.com/isar/hive) 本项目持久化储存能力来自 hive。 ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } android { namespace "com.example.kazumi" compileSdk flutter.compileSdkVersion // Pin ndk version to 21.3.6528147 to fix android build warning // The build warning throws by gradle plugin, when the ndk is older than gradle plugin required version ndkVersion "27.2.12479018" compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.predidit.kazumi" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies {} // F-droid splits APKs by ABI, and requires different versionCode for each ABI. // For flutter version X.Y.Z, version code is X0Y0ZA, where A is the ABI code. // See: // * https://developer.android.com/build/gradle-tips // * https://developer.android.com/studio/build/configure-apk-splits // * https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/im.nfc.nfsee.yml ext.abiCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86_64": 4] import com.android.build.OutputFile android.applicationVariants.all { variant -> variant.outputs.each { output -> def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) if (abiVersionCode != null) { output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode } } } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/example/kazumi/MainActivity.kt ================================================ package com.example.kazumi import android.content.Intent import android.os.Build import android.os.StatFs import android.net.Uri import android.os.Bundle import androidx.annotation.NonNull import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { private val CHANNEL = "com.predidit.kazumi/intent" private val STORAGE_CHANNEL = "com.predidit.kazumi/storage" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "openWithMime") { val url = call.argument("url") val mimeType = call.argument("mimeType") if (url != null && mimeType != null) { openWithMime(url, mimeType) result.success(null) } else { result.error("INVALID_ARGUMENT", "URL and MIME type required", null) } } else if (call.method == "checkIfInMultiWindowMode") { val isInMultiWindow = checkIfInMultiWindowMode() result.success(isInMultiWindow) } else if (call.method == "getAndroidSdkVersion") { val sdkVersion = getAndroidSdkVersion() result.success(sdkVersion) } else { result.notImplemented() } } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, STORAGE_CHANNEL).setMethodCallHandler { call, result -> if (call.method == "getAvailableStorage") { val path = call.argument("path") ?: filesDir.absolutePath val availableBytes = getAvailableStorage(path) result.success(availableBytes) } else { result.notImplemented() } } } private fun openWithMime(url: String, mimeType: String) { val intent = Intent() intent.action = Intent.ACTION_VIEW intent.setDataAndType(Uri.parse(url), mimeType) startActivity(intent) } private fun checkIfInMultiWindowMode(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { this.isInMultiWindowMode } else { false } } private fun getAndroidSdkVersion(): Int { return Build.VERSION.SDK_INT } private fun getAvailableStorage(path: String): Long { return try { val stat = StatFs(path) stat.availableBlocksLong * stat.blockSizeLong } catch (e: Exception) { -1L } } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/values/colors.xml ================================================ #ffffff ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath } settings.ext.flutterSdkPath = flutterSdkPath() includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.7.0" apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" ================================================ FILE: assets/bbcode/BBCode.g4 ================================================ grammar BBCode; options { language=Dart; } document : element* EOF ; element : tag | plain | bgm | sticker ; tag : '[' tagName=STRING ('=' attr=STRING)? ']' content=element* '[/' STRING ']' ; plain : (STRING | '=' | '/' | '[' | ']' | '(' | ')')+ // workaround unless these will break tag reconginze | '[来自Bangumi for android]' | '[来自Bangumi for iOS]' ; bgm : ('(bgm' | '(BGM') id=STRING ')' ; sticker : '(=A=)' | '(=w=)' | '(-w=)' | '(S_S)' | '(=v=)' | '(@_@)' | '(=W=)' | '(TAT)' | '(T_T)' | '(=\'=)' | '(=3=)' | '(= =\')' | '(=///=)' | '(=.,=)' | '(:P)' | '(LOL)'; STRING : ~[=[\]()]+; ================================================ FILE: assets/linux/DEBIAN/postinst ================================================ #!/usr/bin/env sh ln -sf /opt/Kazumi/kazumi /usr/bin/kazumi chmod +x /usr/bin/kazumi update-mime-database /usr/share/mime || true update-desktop-database /usr/share/applications || true exit 0 ================================================ FILE: assets/linux/DEBIAN/postrm ================================================ #!/usr/bin/env sh rm /usr/bin/kazumi update-mime-database /usr/share/mime || true update-desktop-database /usr/share/applications || true exit 0 ================================================ FILE: assets/linux/io.github.Predidit.Kazumi.desktop ================================================ [Desktop Entry] Name=Kazumi Comment=Watch Animes online with danmaku support. Comment[zh_CN]=一款好用的追番软件 Exec=kazumi StartupWMClass=kazumi Icon=io.github.Predidit.Kazumi Terminal=false Type=Application Categories=AudioVideo;Audio;Video; ================================================ FILE: assets/plugins/7sefun.json ================================================ { "api": "4", "type": "anime", "name": "7sefun", "version": "1.2", "muliSources": true, "useWebview": true, "useNativePlayer": true, "userAgent": "", "baseURL": "https://www.7sefun.top/", "searchURL": "https://www.7sefun.top/vodsearch/-------------.html?wd=@keyword", "searchList": "//div[2]/div[2]/div[2]/div[2]/div", "searchName": "//div[2]/text()", "searchResult": "//a", "chapterRoads": "//div[2]/div[2]/div[2]/div/div[2]/div[1]/div[2]", "chapterResult": "//a" } ================================================ FILE: assets/plugins/AGE.json ================================================ { "api": "1", "type": "anime", "name": "AGE", "version": "1.5", "muliSources": true, "useWebview": true, "useNativePlayer": true, "userAgent": "", "baseURL": "https://www.agedm.io/", "searchURL": "https://www.agedm.io/search?query=@keyword", "searchList": "//div[2]/div/section/div/div/div/div", "searchName": "//div/div[2]/h5/a", "searchResult": "//div/div[2]/h5/a", "chapterRoads": "//div[2]/div/section/div/div[2]/div[2]/div[2]/div", "chapterResult": "//ul/li/a" } ================================================ FILE: assets/plugins/DM84.json ================================================ { "api": "5", "type": "anime", "name": "DM84", "version": "1.4", "muliSources": true, "useWebview": true, "useNativePlayer": true, "userAgent": "", "adBlocker": true, "baseURL": "https://dmbus.cc/", "searchURL":"https://dmbus.cc/s----------.html?wd=@keyword", "searchList": "//div/div[3]/ul/li", "searchName": "//div/a[2]", "searchResult": "//div/a[2]", "chapterRoads": "//div/div[4]/div/ul", "chapterResult": "//li/a" } ================================================ FILE: assets/shaders/Anime4K_AutoDownscalePre_x2.glsl ================================================ // This is free and unencumbered software released into the public domain. // Anyone is free to copy, modify, publish, use, compile, sell, or // distribute this software, either in source code form or as a compiled // binary, for any purpose, commercial or non-commercial, and by any // means. // In jurisdictions that recognize copyright laws, the author or authors // of this software dedicate any and all copyright interest in the // software to the public domain. We make this dedication for the benefit // of the public at large and to the detriment of our heirs and // successors. We intend this dedication to be an overt act of // relinquishment in perpetuity of all present and future rights to this // software under copyright law. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // For more information, please refer to //!DESC Anime4K-v4.0-AutoDownscalePre-x2 //!HOOK MAIN //!BIND HOOKED //!BIND NATIVE //!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 > * * //!WIDTH OUTPUT.w //!HEIGHT OUTPUT.h vec4 hook() { return HOOKED_tex(HOOKED_pos); } ================================================ FILE: assets/shaders/Anime4K_AutoDownscalePre_x4.glsl ================================================ // This is free and unencumbered software released into the public domain. // Anyone is free to copy, modify, publish, use, compile, sell, or // distribute this software, either in source code form or as a compiled // binary, for any purpose, commercial or non-commercial, and by any // means. // In jurisdictions that recognize copyright laws, the author or authors // of this software dedicate any and all copyright interest in the // software to the public domain. We make this dedication for the benefit // of the public at large and to the detriment of our heirs and // successors. We intend this dedication to be an overt act of // relinquishment in perpetuity of all present and future rights to this // software under copyright law. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // For more information, please refer to //!DESC Anime4K-v3.2-AutoDownscalePre-x4 //!HOOK MAIN //!BIND HOOKED //!BIND NATIVE //!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 > * * //!WIDTH OUTPUT.w 2 / //!HEIGHT OUTPUT.h 2 / vec4 hook() { return HOOKED_tex(HOOKED_pos); } ================================================ FILE: assets/shaders/Anime4K_Clamp_Highlights.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-De-Ring-Compute-Statistics //!HOOK MAIN //!BIND HOOKED //!SAVE STATSMAX //!COMPONENTS 1 #define KERNELSIZE 5 //Kernel size, must be an positive odd integer. #define KERNELHALFSIZE 2 //Half of the kernel size without remainder. Must be equal to trunc(KERNELSIZE/2). float get_luma(vec4 rgba) { return dot(vec4(0.299, 0.587, 0.114, 0.0), rgba); } vec4 hook() { float gmax = 0.0; for (int i=0; iRGB matrix has 1 for every row... (which is the case for BT.709) //Otherwise we would need to convert RGB to YUV, modify Y then convert back to RGB. return HOOKED_tex(HOOKED_pos) - (current_luma - new_luma); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_M.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.061233472, 0.39222646, 0.029704979, 0.02586828); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.018297346, -0.080951825, -0.062163066, -0.08050014); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.008952847, -0.0058945753, -0.08097229, 0.020968592); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.059377354, -0.02055341, 0.07234869, -0.015452986); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_3_tf //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.048888464, -0.0561434, 0.030690912, -0.030496685); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_4_tf //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.011169491, 0.032399546, 0.138099, 0.023857072); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_5_tf //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.013687534, -0.08185164, -0.04755438, 0.290178); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(M)-Conv-3x1x1x56 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_tf //!BIND conv2d_1_tf //!BIND conv2d_2_tf //!BIND conv2d_3_tf //!BIND conv2d_4_tf //!BIND conv2d_5_tf //!BIND conv2d_6_tf //!SAVE MAIN //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(-0.010478934, -0.008364784, -0.010246552, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_S.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.011108127, -0.07481861, 0.07640154, 0.4964964); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.16371979, -0.024620198, -0.035754893, 0.04176776); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.0036821906, -0.050239526, -0.01355402, 0.00048220603); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(S)-Conv-3x3x3x8 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_2_tf //!SAVE MAIN //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.0052927984, -0.0060193934, -0.0048643993, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Restore_CNN_VL.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.046043985, 0.055581126, -0.08791638, -0.13022089); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf1 //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.0022791293, -0.024132347, -0.57621074, 0.028573977); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.018166734, -0.11002478, -0.05554318, -0.0988193); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.022609137, -0.028548084, 0.024431901, 0.010504478); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.022956269, 0.029688787, -0.070148066, -0.07163476); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf1 //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.103826486, 0.045373913, 0.11565896, -0.06568643); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.0001705175, -0.031081453, 0.010100773, -0.027214011); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf1 //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.012053958, -4.6962363e-05, 0.0020099226, -0.033494607); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.034743138, 0.012946433, -0.082333155, 0.07721756); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf1 //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.08895955, -0.027667087, 0.20500831, 0.00037762933); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.043347504, -0.20504741, -0.037821215, -0.014486937); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf1 //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.011865144, 0.11717201, -0.13823777, -0.059450272); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.02981662, -0.26338395, -0.011632586, 0.15063232); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf1 //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.18765923, -0.07697714, 0.028134674, -0.060966115); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_7_tf //!WIDTH conv2d_6_tf.w //!HEIGHT conv2d_6_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.19233678, 0.016725872, -0.008011114, -0.1977463); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_7_tf1 //!WIDTH conv2d_6_tf.w //!HEIGHT conv2d_6_tf.h //!COMPONENTS 4 #define go_0(x_off, y_off) (max((conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_6_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_6_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.26886886, 0.05874665, 0.10268232, 0.05833081); return result; } //!DESC Anime4K-v4.0-Restore-CNN-(VL)-Conv-3x1x1x112 //!HOOK MAIN //!BIND MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!BIND conv2d_7_tf //!BIND conv2d_7_tf1 //!SAVE MAIN //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h #define g_0 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_1 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_8 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_9 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_12 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_13 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_16 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_17 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_20 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_21 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_24 (max((conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0)) #define g_25 (max((conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_7_tf_tex(conv2d_7_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_7_tf1_tex(conv2d_7_tf1_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(0.047567394, -0.02504617, -0.028163986, 0.0); return result + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_M.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.014463938, -0.0031906287, 0.007015422, -0.003888468); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.032014452, -0.020821465, 0.0826416, -0.002838458); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.0766796, 0.08115133, -0.05703058, 0.14025708); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.05101961, -0.060740646, -0.024465766, 0.058471628); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_3_tf //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.0015292584, -0.043625206, -0.09429898, -0.06280405); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_4_tf //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.04400571, -0.04015565, 0.0140529545, 0.05474095); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_5_tf //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.02021373, 0.0014037411, 0.0012718709, 0.017278494); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Conv-4x1x1x56 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_1_tf //!BIND conv2d_2_tf //!BIND conv2d_3_tf //!BIND conv2d_4_tf //!BIND conv2d_5_tf //!BIND conv2d_6_tf //!SAVE conv2d_last_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_2 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_4 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_5 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_6 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_8 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_9 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_10 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_12 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_13 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(0.016836504, 0.010161949, 0.021351453, 0.01278978); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(M)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; float c1 = c0; float c2 = c1; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_S.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.01050964, -0.00939481, 0.17684458, 0.027366742); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_tf //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.012077211, 0.013045883, 0.0380778, -0.02908858); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_1_tf //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.018736731, -0.0026039074, 0.050130025, -0.055364225); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Conv-4x3x3x8 //!HOOK MAIN //!BIND conv2d_2_tf //!SAVE conv2d_last_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.00346731, -0.0046263863, -0.004627155, -0.0057769152); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(S)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; float c1 = c0; float c2 = c1; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/Anime4K_Upscale_CNN_x2_VL.glsl ================================================ // MIT License // Copyright (c) 2019-2021 bloc97 // All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.0072128535, -0.05658625, 0.052939568, -0.1760861); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x3 //!HOOK MAIN //!BIND MAIN //!SAVE conv2d_tf1 //!WIDTH MAIN.w //!HEIGHT MAIN.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (MAIN_texOff(vec2(x_off, y_off))) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.007717166, -0.027800834, 0.0795002, 0.0053199283); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.0216415, 0.09015036, -0.030761974, -0.26541537); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!SAVE conv2d_1_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.060742114, -0.037577342, 0.055704296, 0.03134311); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.06427697, -0.00039365015, 0.011889719, 0.060232002); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!SAVE conv2d_2_tf1 //!WIDTH conv2d_1_tf.w //!HEIGHT conv2d_1_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_1_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_1_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.0063728676, -0.029053684, -0.052831043, 0.006475641); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.056541316, 0.041788545, -0.036094554, -0.021763096); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!SAVE conv2d_3_tf1 //!WIDTH conv2d_2_tf.w //!HEIGHT conv2d_2_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_2_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_2_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.07722432, -0.025165567, 0.034291282, -0.09902708); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.04244928, -0.014280219, 0.017129054, -0.08807801); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!SAVE conv2d_4_tf1 //!WIDTH conv2d_3_tf.w //!HEIGHT conv2d_3_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_3_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_3_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.14375664, -0.0056876075, 0.052177623, 0.07152566); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.024455175, 0.01669877, -0.066231176, 0.036848705); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!SAVE conv2d_5_tf1 //!WIDTH conv2d_4_tf.w //!HEIGHT conv2d_4_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_4_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_4_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.023832673, 0.03702965, -0.04749135, -0.10982549); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(0.00590038, 0.03082865, 0.002111702, -0.03330112); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x3x3x16 //!HOOK MAIN //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!SAVE conv2d_6_tf1 //!WIDTH conv2d_5_tf.w //!HEIGHT conv2d_5_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define go_0(x_off, y_off) (max((conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_1(x_off, y_off) (max((conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) #define go_2(x_off, y_off) (max(-(conv2d_5_tf_texOff(vec2(x_off, y_off))), 0.0)) #define go_3(x_off, y_off) (max(-(conv2d_5_tf1_texOff(vec2(x_off, y_off))), 0.0)) vec4 hook() { 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); result += vec4(-0.03573331, 0.032919675, 0.011109369, 0.008329268); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(-0.10795144, -0.09953324, -0.055413827, -0.03875493); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf1 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(0.06026231, 0.040204916, 0.037672628, 0.023496555); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Conv-4x1x1x112 //!HOOK MAIN //!BIND conv2d_tf //!BIND conv2d_tf1 //!BIND conv2d_1_tf //!BIND conv2d_1_tf1 //!BIND conv2d_2_tf //!BIND conv2d_2_tf1 //!BIND conv2d_3_tf //!BIND conv2d_3_tf1 //!BIND conv2d_4_tf //!BIND conv2d_4_tf1 //!BIND conv2d_5_tf //!BIND conv2d_5_tf1 //!BIND conv2d_6_tf //!BIND conv2d_6_tf1 //!SAVE conv2d_last_tf2 //!WIDTH conv2d_tf.w //!HEIGHT conv2d_tf.h //!COMPONENTS 4 //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * #define g_0 (max((conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_1 (max((conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_2 (max(-(conv2d_tf_tex(conv2d_tf_pos)), 0.0)) #define g_3 (max(-(conv2d_tf1_tex(conv2d_tf1_pos)), 0.0)) #define g_4 (max((conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_5 (max((conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_6 (max(-(conv2d_1_tf_tex(conv2d_1_tf_pos)), 0.0)) #define g_7 (max(-(conv2d_1_tf1_tex(conv2d_1_tf1_pos)), 0.0)) #define g_8 (max((conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_9 (max((conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_10 (max(-(conv2d_2_tf_tex(conv2d_2_tf_pos)), 0.0)) #define g_11 (max(-(conv2d_2_tf1_tex(conv2d_2_tf1_pos)), 0.0)) #define g_12 (max((conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_13 (max((conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_14 (max(-(conv2d_3_tf_tex(conv2d_3_tf_pos)), 0.0)) #define g_15 (max(-(conv2d_3_tf1_tex(conv2d_3_tf1_pos)), 0.0)) #define g_16 (max((conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_17 (max((conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_18 (max(-(conv2d_4_tf_tex(conv2d_4_tf_pos)), 0.0)) #define g_19 (max(-(conv2d_4_tf1_tex(conv2d_4_tf1_pos)), 0.0)) #define g_20 (max((conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_21 (max((conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_22 (max(-(conv2d_5_tf_tex(conv2d_5_tf_pos)), 0.0)) #define g_23 (max(-(conv2d_5_tf1_tex(conv2d_5_tf1_pos)), 0.0)) #define g_24 (max((conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_25 (max((conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) #define g_26 (max(-(conv2d_6_tf_tex(conv2d_6_tf_pos)), 0.0)) #define g_27 (max(-(conv2d_6_tf1_tex(conv2d_6_tf1_pos)), 0.0)) vec4 hook() { 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; result += vec4(0.069033325, 0.040207114, 0.027286075, 0.0065334598); return result; } //!DESC Anime4K-v3.2-Upscale-CNN-x2-(VL)-Depth-to-Space //!HOOK MAIN //!BIND MAIN //!BIND conv2d_last_tf //!BIND conv2d_last_tf1 //!BIND conv2d_last_tf2 //!SAVE MAIN //!WIDTH conv2d_last_tf.w 2 * //!HEIGHT conv2d_last_tf.h 2 * //!WHEN OUTPUT.w MAIN.w / 1.200 > OUTPUT.h MAIN.h / 1.200 > * vec4 hook() { vec2 f0 = fract(conv2d_last_tf_pos * conv2d_last_tf_size); ivec2 i0 = ivec2(f0 * vec2(2.0)); float c0 = conv2d_last_tf_tex((vec2(0.5) - f0) * conv2d_last_tf_pt + conv2d_last_tf_pos)[i0.y * 2 + i0.x]; vec2 f1 = fract(conv2d_last_tf1_pos * conv2d_last_tf1_size); ivec2 i1 = ivec2(f1 * vec2(2.0)); float c1 = conv2d_last_tf1_tex((vec2(0.5) - f1) * conv2d_last_tf1_pt + conv2d_last_tf1_pos)[i1.y * 2 + i1.x]; vec2 f2 = fract(conv2d_last_tf2_pos * conv2d_last_tf2_size); ivec2 i2 = ivec2(f2 * vec2(2.0)); float c2 = conv2d_last_tf2_tex((vec2(0.5) - f2) * conv2d_last_tf2_pt + conv2d_last_tf2_pos)[i2.y * 2 + i2.x]; float c3 = c2; return vec4(c0, c1, c2, c3) + MAIN_tex(MAIN_pos); } ================================================ FILE: assets/shaders/LICENSE ================================================ MIT License Copyright (c) 2019 bloc97 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: assets/statements/statements.txt ================================================ 在使用本软件之前,请您仔细阅读以下内容,并确保您充分理解并同意以下条款: 1、本软件为开源软件,您应该免费获取和使用。如果您是从第三方付费获取,建议您向其索取赔偿。 2、本软件完全基于您个人意愿使用,您应该对自己的使用行为和所有结果承担全部责任。 3、本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。 4、本软件并不保证与所有操作系统或硬件设备兼容。本软件作者或贡献者不对因使用本软件而产生的任何技术或安全问题承担责任。 5、本软件作者或贡献者不承担因使用本软件而造成的任何直接、间接、特殊或后果性的损失或损害的责任,包括但不限于财产损失、商业利润损失、信息或数据丢失或损坏等。 6、本软件使用者应遵守国家相关法律法规和使用规范,不得利用本软件从事任何违法违规行为。如因使用本软件而导致的违法行为,使用者应承担相应的法律责任。 7、本软件不会收集、存储、使用任何用户的个人信息,包括但不限于姓名、地址、电子邮件地址、电话号码等。在使用本软件过程中,不会进行任何形式的个人信息采集。 8、本软件作者或贡献者保留随时修改、增加、删除本免责声明中的内容而不另行通知的权利。 9、如果本软件存在侵犯您的合法权益的情况,请及时与作者联系,作者将会及时删除有关内容。 如您不同意本免责声明中的任何内容,请勿使用本软件。使用本软件即代表您已完全理解并同意上述内容。 ================================================ FILE: devtools_options.yaml ================================================ extensions: ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ 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 (~ ̄▽ ̄)~ ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ An anime collection APP based on custom rules. ================================================ FILE: fastlane/metadata/android/en-US/title.txt ================================================ Kazumi ================================================ FILE: fastlane/metadata/android/zh-CN/full_description.txt ================================================ 使用 flutter 开发的基于自定义规则的番剧采集与在线观看程序。使用最多五行基于 Xpath 语法的选择器构建自己的规则。支持规则导入与规则分享。绝赞开发中 (~ ̄▽ ̄)~ ================================================ FILE: fastlane/metadata/android/zh-CN/short_description.txt ================================================ 基于自定义规则的番剧采集APP,支持流媒体在线观看,支持弹幕。 ================================================ FILE: fastlane/metadata/android/zh-CN/title.txt ================================================ Kazumi ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter import AVKit @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) let channel = FlutterMethodChannel( name: "com.predidit.kazumi/intent", binaryMessenger: engineBridge.applicationRegistrar.messenger() ) channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in if call.method == "openWithReferer" { guard let args = call.arguments else { return } if let myArgs = args as? [String: Any], let url = myArgs["url"] as? String, let referer = myArgs["referer"] as? String { self?.openVideoWithReferer(url: url, referer: referer) } result(nil) } else { result(FlutterMethodNotImplemented) } } let storageChannel = FlutterMethodChannel( name: "com.predidit.kazumi/storage", binaryMessenger: engineBridge.applicationRegistrar.messenger() ) storageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in if call.method == "getAvailableStorage" { do { let attrs = try FileManager.default.attributesOfFileSystem( forPath: NSHomeDirectory() ) if let freeSize = attrs[.systemFreeSize] as? Int64 { result(freeSize) } else { result(-1) } } catch { result(-1) } } else { result(FlutterMethodNotImplemented) } } } // TODO: ADD VLC SUPPORT // VLC can be downloaded from iOS App Store, but don't know how to build selectable app lists, while checking if it is installled. // VLC supports more video formats than AVPlayer but does not support referer while AVPlayer does private func openVideoWithReferer(url: String, referer: String) { guard let videoUrl = URL(string: url) else { return } let headers: [String: String] = [ "Referer": referer, ] let asset = AVURLAsset(url: videoUrl, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) let playerItem = AVPlayerItem(asset: asset) let player = AVPlayer(playerItem: playerItem) let playerViewController = AVPlayerViewController() playerViewController.player = player playerViewController.videoGravity = AVLayerVideoGravity.resizeAspect // Use UIScene API instead of deprecated keyWindow guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController else { return } rootViewController.present(playerViewController, animated: true) { playerViewController.player?.play() } // guard let appURL = URL(string: "vlc-x-callback://x-callback-url/stream?url=" + url) else { // return // } // if UIApplication.shared.canOpenURL(appURL) && referer.isEmpty { // UIApplication.shared.open(appURL, options: [:], completionHandler: nil) // } } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json ================================================ { "images" : [ { "filename" : "background.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "darkbackground.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "filename" : "LaunchImage.png", "idiom" : "universal", "scale" : "1x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "LaunchImage@2x.png", "idiom" : "universal", "scale" : "2x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "LaunchImage@3x.png", "idiom" : "universal", "scale" : "3x" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "LaunchImageDark@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You 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. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ NSPhotoLibraryAddUsageDescription CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Kazumi CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName kazumi CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSApplicationQueriesSchemes vlc-x-callback LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneClassName UIWindowScene UISceneDelegateClassName FlutterSceneDelegate UISceneConfigurationName flutter UISceneStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIStatusBarHidden ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: lib/app_module.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/index_module.dart'; class AppModule extends Module { @override void binds(i) { } @override void routes(r) { r.module("/", module: IndexModule()); } } ================================================ FILE: lib/app_widget.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:window_manager/window_manager.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/settings/theme_provider.dart'; import 'package:provider/provider.dart'; import 'package:kazumi/utils/constants.dart'; class AppWidget extends StatefulWidget { const AppWidget({super.key}); @override State createState() => _AppWidgetState(); } class _AppWidgetState extends State with TrayListener, WidgetsBindingObserver, WindowListener { Box setting = GStorage.setting; final TrayManager trayManager = TrayManager.instance; bool showingExitDialog = false; @override void initState() { trayManager.addListener(this); windowManager.addListener(this); setPreventClose(); WidgetsBinding.instance.addObserver(this); super.initState(); } void setPreventClose() async { if (Utils.isDesktop()) { await windowManager.setPreventClose(true); setState(() {}); } } @override void dispose() { trayManager.removeListener(this); windowManager.removeListener(this); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void onTrayIconMouseDown() { windowManager.show(); } @override void onTrayIconRightMouseDown() { trayManager.popUpContextMenu(); } @override void onTrayMenuItemClick(MenuItem menuItem) { switch (menuItem.key) { case 'show_window': windowManager.show(); case 'exit': exit(0); } } /// 处理窗口关闭事件, /// 需要使用 `windowManager.close()` 来触发,`exit(0)` 会直接退出程序 @override void onWindowClose() { final setting = GStorage.setting; final exitBehavior = setting.get(SettingBoxKey.exitBehavior, defaultValue: 2); switch (exitBehavior) { case 0: exit(0); case 1: KazumiDialog.dismiss(); windowManager.hide(); break; default: if (showingExitDialog) return; showingExitDialog = true; KazumiDialog.show(onDismiss: () { showingExitDialog = false; }, builder: (context) { bool saveExitBehavior = false; // 下次不再询问? return AlertDialog( title: const Text('退出确认'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('您想要退出 Kazumi 吗?'), const SizedBox(height: 24), StatefulBuilder(builder: (context, setState) { onChanged(value) { saveExitBehavior = value ?? false; setState(() {}); } return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ Checkbox(value: saveExitBehavior, onChanged: onChanged), const Text('下次不再询问'), ], ); }), ], ), actions: [ TextButton( onPressed: () async { if (saveExitBehavior) { await setting.put(SettingBoxKey.exitBehavior, 0); } exit(0); }, child: const Text('退出 Kazumi')), TextButton( onPressed: () async { if (saveExitBehavior) { await setting.put(SettingBoxKey.exitBehavior, 1); } KazumiDialog.dismiss(); windowManager.hide(); }, child: const Text('最小化至托盘')), const TextButton( onPressed: KazumiDialog.dismiss, child: Text('取消')), ], ); }); } } /// 处理前后台变更 /// windows/linux 在程序后台或失去焦点时只会触发 inactive 不会触发 paused /// android/ios/macos 在程序后台时会先触发 inactive 再触发 paused, 回到前台时会先触发 inactive 再触发 resumed @override void didChangeAppLifecycleState(AppLifecycleState state) async { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.paused) { KazumiLogger() .i("AppLifecycleState.paused: Application moved to background"); } else if (state == AppLifecycleState.resumed) { KazumiLogger() .i("AppLifecycleState.resumed: Application moved to foreground"); } else if (state == AppLifecycleState.inactive) { KazumiLogger().i("AppLifecycleState.inactive: Application is inactive"); } } Future _handleTray() async { if (Platform.isWindows) { await trayManager.setIcon('assets/images/logo/logo_lanczos.ico'); } else if (Platform.environment.containsKey('FLATPAK_ID') || Platform.environment.containsKey('SNAP')) { await trayManager.setIcon('io.github.Predidit.Kazumi'); } else { await trayManager.setIcon('assets/images/logo/logo_rounded.png'); } if (!Platform.isLinux) { await trayManager.setToolTip('Kazumi'); } Menu trayMenu = Menu(items: [ MenuItem(key: 'show_window', label: '显示窗口'), MenuItem.separator(), MenuItem(key: 'exit', label: '退出 Kazumi') ]); await trayManager.setContextMenu(trayMenu); } @override Widget build(BuildContext context) { final ThemeProvider themeProvider = Provider.of(context); if (Utils.isDesktop()) { _handleTray(); } dynamic color; dynamic defaultThemeColor = setting.get(SettingBoxKey.themeColor, defaultValue: 'default'); if (defaultThemeColor == 'default') { color = Colors.green; } else { color = Color(int.parse(defaultThemeColor, radix: 16)); } bool oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false); bool useSystemFont = setting.get(SettingBoxKey.useSystemFont, defaultValue: false); final defaultThemeMode = setting.get(SettingBoxKey.themeMode, defaultValue: 'system'); if (defaultThemeMode == 'dark') { themeProvider.setThemeMode(ThemeMode.dark, notify: false); } if (defaultThemeMode == 'light') { themeProvider.setThemeMode(ThemeMode.light, notify: false); } if (defaultThemeMode == 'system') { themeProvider.setThemeMode(ThemeMode.system, notify: false); } themeProvider.setFontFamily(useSystemFont, notify: false); var defaultDarkTheme = ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.dark, colorSchemeSeed: color, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024); var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme); themeProvider.setTheme( ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.light, colorSchemeSeed: color, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024), oledEnhance ? oledDarkTheme : defaultDarkTheme, notify: false, ); var app = DynamicColorBuilder( builder: (theme, darkTheme) { if (themeProvider.useDynamicColor) { themeProvider.setTheme( ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, colorScheme: theme, brightness: Brightness.light, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024), oledEnhance ? Utils.oledDarkTheme(ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, colorScheme: darkTheme, brightness: Brightness.dark, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024)) : ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, colorScheme: darkTheme, brightness: Brightness.dark, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024), notify: false, ); } return MaterialApp.router( title: "Kazumi", localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [ Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN") ], locale: const Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"), theme: themeProvider.light, darkTheme: themeProvider.dark, themeMode: themeProvider.themeMode, routerConfig: Modular.routerConfig, ); }, ); Modular.setObservers([KazumiDialog.observer]); // 强制设置高帧率 if (Platform.isAndroid) { try { late List modes; FlutterDisplayMode.supported.then((value) { modes = value; var storageDisplay = setting.get(SettingBoxKey.displayMode); DisplayMode f = DisplayMode.auto; if (storageDisplay != null) { f = modes.firstWhere((e) => e.toString() == storageDisplay); } DisplayMode preferred = modes.toList().firstWhere((el) => el == f); FlutterDisplayMode.setPreferredMode(preferred); }); } catch (e) { KazumiLogger().e('DisPlay: set preferred mode failed', error: e); } } return app; } } ================================================ FILE: lib/bbcode/README.md ================================================ # 基于 antlr4 的 BBCode 解析 ## 相关文件 - [assets/bbcode/BBCode.g4](../../assets/bbcode/BBCode.g4): antlr4 语法文件 - [lib/bbcode/generated](../../lib/bbcode/generated): antlr4 生成的 dart 代码所在文件夹 ## 关键文件 - [lib/bbcode/bbcode_elements.dart](bbcode_elements.dart): BBCode 元素 - [lib/bbcode/bbcode_base_listener.dart](bbcode_base_listener.dart): BBCode 解析器的入口文件 - [lib/bbcode/bbcode_widget.dart](bbcode_widget.dart): BBCode 组件 ## 如何开发 ### 配置环境 1. 根据[官方文档](https://github.com/antlr/antlr4/blob/dev/doc/dart-target.md)配置环境 2. 在 IDE 中安装 `antlr v4` 插件 ### 开发 1. 修改 [assets/bbcode/BBCode.g4](../../assets/bbcode/BBCode.g4) 文件,通过插件的 Preview 功能确定解析是否正确 2. 通过该文件生成新的 dart 文件到 [lib/bbcode/generated](../../lib/bbcode/generated) 文件夹内,删除无用文件 3. 参考文件内的注释进行修改 ### 测试 BBCode ```dart import 'package:flutter/material.dart'; import 'bbcode/bbcode_widget.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('BBCode Parser')), body: Card( color: Theme.of(context).colorScheme.secondaryContainer, child: const Padding( padding: EdgeInsets.all(8.0), child: Padding( padding: EdgeInsets.all(16), child: BBCodeWidget( bbcode: '[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]', ), ), ), ), ), ); } } ``` ================================================ FILE: lib/bbcode/bbcode_base_listener.dart ================================================ import 'package:flutter/material.dart'; import 'package:antlr4/antlr4.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/bbcode/bbcode_elements.dart'; import 'generated/BBCodeListener.dart'; import 'generated/BBCodeParser.dart'; class BBCodeBaseListener implements BBCodeListener { final List bbcode = []; /// 记录进入标签时的位置 void _enterTag(TagContext ctx) { final tagName = ctx.tagName?.text; switch (tagName) { case 'URL': case 'url': bbCodeTag.link = bbcode.length; break; case 'USER': case 'user': bbCodeTag.link = bbcode.length; break; case 'QUOTE': case 'quote': bbCodeTag.quoted = bbcode.length; break; case 'B': case 'b': bbCodeTag.bold = bbcode.length; break; case 'I': case 'i': bbCodeTag.italic = bbcode.length; break; case 'S': case 's': bbCodeTag.strikeThrough = bbcode.length; break; case 'U': case 'u': bbCodeTag.underline = bbcode.length; break; case 'PHOTO': case 'photo': case 'IMG': case 'img': bbCodeTag.img = bbcode.length; break; case 'MASK': case 'mask': bbCodeTag.masked = bbcode.length; break; case 'SIZE': case 'size': bbCodeTag.size = bbcode.length; break; case 'COLOR': case 'color': bbCodeTag.color = bbcode.length; break; default: KazumiLogger() .e('BBCode: unrecognized Tag: ${ctx.text}, please submit an issue with logs, bangumi, and episode information'); break; } } /// 对标签内所有的 BBCodeText 叠加样式 void _exitTag(TagContext ctx) { final tagName = ctx.tagName?.text; switch (tagName) { case 'URL': case 'url': if (bbcode.isNotEmpty && bbcode[bbCodeTag.link!] is BBCodeText) { if (ctx.attr != null) { bbcode[bbCodeTag.link!].link = ctx.attr!.text; } else { bbcode[bbCodeTag.link!].link = bbcode[bbCodeTag.link!].text; } } break; case 'USER': case 'user': if (bbcode.isNotEmpty && ctx.attr != null && bbcode[bbCodeTag.link!] is BBCodeText) { bbcode[bbCodeTag.link!].link = 'https://bangumi.tv/user/${ctx.attr!.text}'; bbcode[bbCodeTag.link!].text = '@${bbcode[bbCodeTag.link!].text}'; } break; case 'QUOTE': case 'quote': for (int i = bbCodeTag.quoted!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].quoted = true; } } // Add icon to the end of quoted text bbcode.add(const Icon(Icons.format_quote)); break; case 'B': case 'b': for (int i = bbCodeTag.bold!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].bold = true; } } break; case 'I': case 'i': for (int i = bbCodeTag.italic!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].italic = true; } } break; case 'S': case 's': for (int i = bbCodeTag.strikeThrough!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].strikeThrough = true; } } break; case 'U': case 'u': for (int i = bbCodeTag.underline!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].underline = true; } } break; case 'PHOTO': case 'photo': case 'IMG': case 'img': if (bbCodeTag.img! < bbcode.length && bbcode.isNotEmpty && bbcode[bbCodeTag.img!] is BBCodeText) { bbcode[bbCodeTag.img!] = BBCodeImg(imageUrl: bbcode[bbCodeTag.img!].text); } break; case 'MASK': case 'mask': for (int i = bbCodeTag.masked!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].masked = true; } } break; case 'SIZE': case 'size': for (int i = bbCodeTag.size!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].size = int.parse(ctx.attr!.text!); } } break; case 'COLOR': case 'color': for (int i = bbCodeTag.color!; i < bbcode.length; i++) { if (bbcode.isNotEmpty && bbcode[i] is BBCodeText) { bbcode[i].color = ctx.attr?.text; } } break; default: KazumiLogger() .e('BBCode: unrecognized Tag: ${ctx.text}, please submit an issue with logs, bangumi, and episode information'); break; } } @override void enterDocument(DocumentContext ctx) {} @override void exitDocument(DocumentContext ctx) {} @override void enterElement(ElementContext ctx) {} @override void exitElement(ElementContext ctx) {} @override void enterTag(TagContext ctx) { _enterTag(ctx); } @override void exitTag(TagContext ctx) { _exitTag(ctx); } @override void enterPlain(PlainContext ctx) { bbcode.add(BBCodeText(text: ctx.text)); } @override void exitPlain(PlainContext ctx) {} @override void enterBgm(BgmContext ctx) { /// 处理 (bgm35) 类型的表情 bbcode.add(BBCodeBgm(id: int.tryParse(ctx.id!.text!) ?? 0)); } @override void exitBgm(BgmContext ctx) {} @override void enterSticker(StickerContext ctx) { /// 处理 (=A=) 类型的表情 /// ctx.start!.type 为 BBCode.tokens 内的 token 值 bbcode.add(BBCodeSticker(id: ctx.start!.type - 11)); } @override void exitSticker(StickerContext ctx) {} @override void enterEveryRule(ParserRuleContext ctx) {} @override void exitEveryRule(ParserRuleContext ctx) {} @override void visitTerminal(TerminalNode node) {} @override void visitErrorNode(ErrorNode node) {} } ================================================ FILE: lib/bbcode/bbcode_elements.dart ================================================ // 记录进入 tag 时 list 所在位置 class BBCodeTag { int? bold; int? italic; int? underline; int? strikeThrough; int? masked; int? quoted; int? code; int? size; int? color; int? link; int? img; void clear() { bold = null; italic = null; underline = null; strikeThrough = null; masked = null; quoted = null; code = null; size = null; color = null; link = null; img = null; } } class BBCodeText { String text; bool bold = false; bool italic = false; bool underline = false; bool strikeThrough = false; bool masked = false; bool quoted = false; bool code = false; int size = 14; String? color; String? link; BBCodeText({ required this.text, this.bold = false, this.italic = false, this.underline = false, this.strikeThrough = false, this.masked = false, this.quoted = false, this.code = false, this.size = 14, this.color, this.link, }); } class BBCodeBgm { int id; BBCodeBgm({required this.id}); } class BBCodeSticker { int id; BBCodeSticker({required this.id}); } class BBCodeImg { String imageUrl; BBCodeImg({required this.imageUrl}); } BBCodeTag bbCodeTag = BBCodeTag(); ================================================ FILE: lib/bbcode/bbcode_widget.dart ================================================ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:antlr4/antlr4.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; import 'bbcode_base_listener.dart'; import 'bbcode_elements.dart'; import 'generated/BBCodeParser.dart'; import 'generated/BBCodeLexer.dart'; class BBCodeWidget extends StatefulWidget { const BBCodeWidget({super.key, required this.bbcode}); final String bbcode; @override State createState() => _BBCodeWidgetState(); } class _BBCodeWidgetState extends State { bool _isVisible = false; /// color 可以为三种表现形式 /// /// `ARGB: #FFFFFFFF` /// /// `RGB: #FFFFFF` /// /// `NAME: red` /// /// 若全部解析失败则返回 null 使用默认颜色 Color? _parseColor(String hex) { if (hex.startsWith('#')) { hex = hex.replaceFirst('#', ''); if (hex.length == 6) { hex = "FF$hex"; } if (hex.length == 8) { return Color(int.parse(hex, radix: 16)); } } switch (hex) { case 'red': return Colors.red; case 'blue': return Colors.blue; case 'orange': return Colors.orange; case 'green': return Colors.green; case 'grey': return Colors.grey; default: return null; } } @override Widget build(BuildContext context) { BBCodeParser.checkVersion(); BBCodeParser.checkVersion(); final input = InputStream.fromString(widget.bbcode); final lexer = BBCodeLexer(input); final tokens = CommonTokenStream(lexer); final parser = BBCodeParser(tokens); final tree = parser.document(); final bbcodeBaseListener = BBCodeBaseListener(); ParseTreeWalker.DEFAULT.walk(bbcodeBaseListener, tree); bbCodeTag.clear(); return Wrap( children: [ SelectableText.rich( TextSpan( children: bbcodeBaseListener.bbcode.map((e) { if (e is BBCodeText) { Color? textColor = (!_isVisible && e.masked) ? Colors.transparent : (e.link != null) ? Colors.blue : (e.quoted) ? Theme.of(context).colorScheme.outline : (e.color != null) ? _parseColor(e.color!) : null; return TextSpan( text: e.text, mouseCursor: (e.link != null || e.masked) ? SystemMouseCursors.click : SystemMouseCursors.text, recognizer: TapGestureRecognizer() ..onTap = (e.link != null || e.masked) ? () { if ((!e.masked || _isVisible) && e.link != null) { launchUrl(Uri.parse(e.link!)); } else if (e.masked) { setState(() { _isVisible = !_isVisible; }); } } : null, style: TextStyle( fontWeight: (e.bold) ? FontWeight.bold : null, fontStyle: (e.italic) ? FontStyle.italic : null, decoration: TextDecoration.combine([ if (e.underline || e.link != null) TextDecoration.underline, if (e.strikeThrough) TextDecoration.lineThrough, ]), decorationColor: textColor, fontSize: e.size.toDouble(), color: textColor, backgroundColor: (!_isVisible && e.masked) ? Color(0xFF555555) : null, fontFeatures: [FontFeature.tabularFigures()], ), ); } else if (e is BBCodeImg) { return WidgetSpan( child: CachedNetworkImage( imageUrl: e.imageUrl, placeholder: (context, url) => const SizedBox(width: 1, height: 1), errorWidget: (context, error, stackTrace) { return const Text('.'); }, ), ); } else if (e is BBCodeBgm) { String url; if (e.id == 11 || e.id == 23) { url = 'https://bangumi.tv/img/smiles/bgm/${e.id}.gif'; } if (e.id < 24) { url = 'https://bangumi.tv/img/smiles/bgm/${e.id}.png'; } if (e.id < 33) { url = 'https://bangumi.tv/img/smiles/tv/0${e.id - 23}.gif'; } url = 'https://bangumi.tv/img/smiles/tv/${e.id - 23}.gif'; return WidgetSpan( child: CachedNetworkImage( imageUrl: url, placeholder: (context, url) => const SizedBox(width: 1, height: 1), errorWidget: (context, error, stackTrace) { return const Text('.'); }, ), ); } else if (e is BBCodeSticker) { return WidgetSpan( child: CachedNetworkImage( imageUrl: 'https://bangumi.tv/img/smiles/${e.id}.gif', placeholder: (context, url) => const SizedBox(width: 1, height: 1), errorWidget: (context, error, stackTrace) { return const Text('.'); }, ), ); } else { // e is Icon return WidgetSpan( child: Icon( (e as Icon).icon, color: Theme.of(context).colorScheme.outline, ), alignment: PlaceholderAlignment.top, ); } }).toList(), ), selectionHeightStyle: ui.BoxHeightStyle.max, ), ], ); } } ================================================ FILE: lib/bbcode/generated/BBCode.tokens ================================================ T__0=1 T__1=2 T__2=3 T__3=4 T__4=5 T__5=6 T__6=7 T__7=8 T__8=9 T__9=10 T__10=11 T__11=12 T__12=13 T__13=14 T__14=15 T__15=16 T__16=17 T__17=18 T__18=19 T__19=20 T__20=21 T__21=22 T__22=23 T__23=24 T__24=25 T__25=26 T__26=27 STRING=28 '['=1 '='=2 ']'=3 '[/'=4 '/'=5 '('=6 ')'=7 '[来自Bangumi for android]'=8 '[来自Bangumi for iOS]'=9 '(bgm'=10 '(BGM'=11 '(=A=)'=12 '(=w=)'=13 '(-w=)'=14 '(S_S)'=15 '(=v=)'=16 '(@_@)'=17 '(=W=)'=18 '(TAT)'=19 '(T_T)'=20 '(=\'=)'=21 '(=3=)'=22 '(= =\')'=23 '(=///=)'=24 '(=.,=)'=25 '(:P)'=26 '(LOL)'=27 ================================================ FILE: lib/bbcode/generated/BBCodeLexer.dart ================================================ import 'package:antlr4/antlr4.dart'; class BBCodeLexer extends Lexer { static final checkVersion = () => RuntimeMetaData.checkVersion('4.13.2', RuntimeMetaData.VERSION); static final List _decisionToDFA = List.generate( _ATN.numberOfDecisions, (i) => DFA(_ATN.getDecisionState(i), i)); static final PredictionContextCache _sharedContextCache = PredictionContextCache(); static const int TOKEN_T__0 = 1, TOKEN_T__1 = 2, TOKEN_T__2 = 3, TOKEN_T__3 = 4, TOKEN_T__4 = 5, TOKEN_T__5 = 6, TOKEN_T__6 = 7, TOKEN_T__7 = 8, TOKEN_T__8 = 9, TOKEN_T__9 = 10, TOKEN_T__10 = 11, TOKEN_T__11 = 12, TOKEN_T__12 = 13, TOKEN_T__13 = 14, TOKEN_T__14 = 15, TOKEN_T__15 = 16, TOKEN_T__16 = 17, TOKEN_T__17 = 18, TOKEN_T__18 = 19, TOKEN_T__19 = 20, TOKEN_T__20 = 21, TOKEN_T__21 = 22, TOKEN_T__22 = 23, TOKEN_T__23 = 24, TOKEN_T__24 = 25, TOKEN_T__25 = 26, TOKEN_T__26 = 27, TOKEN_STRING = 28; @override final List channelNames = [ 'DEFAULT_TOKEN_CHANNEL', 'HIDDEN' ]; @override final List modeNames = [ 'DEFAULT_MODE' ]; @override final List ruleNames = [ 'T__0', 'T__1', 'T__2', 'T__3', 'T__4', 'T__5', 'T__6', 'T__7', 'T__8', 'T__9', 'T__10', 'T__11', 'T__12', 'T__13', 'T__14', 'T__15', 'T__16', 'T__17', 'T__18', 'T__19', 'T__20', 'T__21', 'T__22', 'T__23', 'T__24', 'T__25', 'T__26', 'STRING' ]; static final List _LITERAL_NAMES = [ null, "'['", "'='", "']'", "'[/'", "'/'", "'('", "')'", "'[\\u6765\\u81EABangumi for android]'", "'[\\u6765\\u81EABangumi for iOS]'", "'(bgm'", "'(BGM'", "'(=A=)'", "'(=w=)'", "'(-w=)'", "'(S_S)'", "'(=v=)'", "'(@_@)'", "'(=W=)'", "'(TAT)'", "'(T_T)'", "'(='=)'", "'(=3=)'", "'(= =')'", "'(=///=)'", "'(=.,=)'", "'(:P)'", "'(LOL)'" ]; static final List _SYMBOLIC_NAMES = [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "STRING" ]; static final Vocabulary VOCABULARY = VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @override Vocabulary get vocabulary { return VOCABULARY; } BBCodeLexer(CharStream input) : super(input) { interpreter = LexerATNSimulator(_ATN, _decisionToDFA, _sharedContextCache, recog: this); } @override List get serializedATN => _serializedATN; @override String get grammarFileName => 'BBCode.g4'; @override ATN getATN() { return _ATN; } static const List _serializedATN = [ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41, 21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,1,0,1,4,0,40,41,61,61, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 5,91,0,0,73,74,5,26469,0,0,74,75,5,33258,0,0,75,76,5,66,0,0,76,77, 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, 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, 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, 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, 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, 0,0,98,99,5,33258,0,0,99,100,5,66,0,0,100,101,5,97,0,0,101,102,5,110, 0,0,102,103,5,103,0,0,103,104,5,117,0,0,104,105,5,109,0,0,105,106, 5,105,0,0,106,107,5,32,0,0,107,108,5,102,0,0,108,109,5,111,0,0,109, 110,5,114,0,0,110,111,5,32,0,0,111,112,5,105,0,0,112,113,5,79,0,0, 113,114,5,83,0,0,114,115,5,93,0,0,115,18,1,0,0,0,116,117,5,40,0,0, 117,118,5,98,0,0,118,119,5,103,0,0,119,120,5,109,0,0,120,20,1,0,0, 0,121,122,5,40,0,0,122,123,5,66,0,0,123,124,5,71,0,0,124,125,5,77, 0,0,125,22,1,0,0,0,126,127,5,40,0,0,127,128,5,61,0,0,128,129,5,65, 0,0,129,130,5,61,0,0,130,131,5,41,0,0,131,24,1,0,0,0,132,133,5,40, 0,0,133,134,5,61,0,0,134,135,5,119,0,0,135,136,5,61,0,0,136,137,5, 41,0,0,137,26,1,0,0,0,138,139,5,40,0,0,139,140,5,45,0,0,140,141,5, 119,0,0,141,142,5,61,0,0,142,143,5,41,0,0,143,28,1,0,0,0,144,145,5, 40,0,0,145,146,5,83,0,0,146,147,5,95,0,0,147,148,5,83,0,0,148,149, 5,41,0,0,149,30,1,0,0,0,150,151,5,40,0,0,151,152,5,61,0,0,152,153, 5,118,0,0,153,154,5,61,0,0,154,155,5,41,0,0,155,32,1,0,0,0,156,157, 5,40,0,0,157,158,5,64,0,0,158,159,5,95,0,0,159,160,5,64,0,0,160,161, 5,41,0,0,161,34,1,0,0,0,162,163,5,40,0,0,163,164,5,61,0,0,164,165, 5,87,0,0,165,166,5,61,0,0,166,167,5,41,0,0,167,36,1,0,0,0,168,169, 5,40,0,0,169,170,5,84,0,0,170,171,5,65,0,0,171,172,5,84,0,0,172,173, 5,41,0,0,173,38,1,0,0,0,174,175,5,40,0,0,175,176,5,84,0,0,176,177, 5,95,0,0,177,178,5,84,0,0,178,179,5,41,0,0,179,40,1,0,0,0,180,181, 5,40,0,0,181,182,5,61,0,0,182,183,5,39,0,0,183,184,5,61,0,0,184,185, 5,41,0,0,185,42,1,0,0,0,186,187,5,40,0,0,187,188,5,61,0,0,188,189, 5,51,0,0,189,190,5,61,0,0,190,191,5,41,0,0,191,44,1,0,0,0,192,193, 5,40,0,0,193,194,5,61,0,0,194,195,5,32,0,0,195,196,5,61,0,0,196,197, 5,39,0,0,197,198,5,41,0,0,198,46,1,0,0,0,199,200,5,40,0,0,200,201, 5,61,0,0,201,202,5,47,0,0,202,203,5,47,0,0,203,204,5,47,0,0,204,205, 5,61,0,0,205,206,5,41,0,0,206,48,1,0,0,0,207,208,5,40,0,0,208,209, 5,61,0,0,209,210,5,46,0,0,210,211,5,44,0,0,211,212,5,61,0,0,212,213, 5,41,0,0,213,50,1,0,0,0,214,215,5,40,0,0,215,216,5,58,0,0,216,217, 5,80,0,0,217,218,5,41,0,0,218,52,1,0,0,0,219,220,5,40,0,0,220,221, 5,76,0,0,221,222,5,79,0,0,222,223,5,76,0,0,223,224,5,41,0,0,224,54, 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, 0,0,0,228,229,1,0,0,0,229,56,1,0,0,0,2,0,228,0 ]; static final ATN _ATN = ATNDeserializer().deserialize(_serializedATN); } ================================================ FILE: lib/bbcode/generated/BBCodeListener.dart ================================================ import 'package:antlr4/antlr4.dart'; import 'BBCodeParser.dart'; /// This abstract class defines a complete listener for a parse tree produced by /// [BBCodeParser]. abstract class BBCodeListener extends ParseTreeListener { /// Enter a parse tree produced by [BBCodeParser.document]. /// [ctx] the parse tree void enterDocument(DocumentContext ctx); /// Exit a parse tree produced by [BBCodeParser.document]. /// [ctx] the parse tree void exitDocument(DocumentContext ctx); /// Enter a parse tree produced by [BBCodeParser.element]. /// [ctx] the parse tree void enterElement(ElementContext ctx); /// Exit a parse tree produced by [BBCodeParser.element]. /// [ctx] the parse tree void exitElement(ElementContext ctx); /// Enter a parse tree produced by [BBCodeParser.tag]. /// [ctx] the parse tree void enterTag(TagContext ctx); /// Exit a parse tree produced by [BBCodeParser.tag]. /// [ctx] the parse tree void exitTag(TagContext ctx); /// Enter a parse tree produced by [BBCodeParser.plain]. /// [ctx] the parse tree void enterPlain(PlainContext ctx); /// Exit a parse tree produced by [BBCodeParser.plain]. /// [ctx] the parse tree void exitPlain(PlainContext ctx); /// Enter a parse tree produced by [BBCodeParser.bgm]. /// [ctx] the parse tree void enterBgm(BgmContext ctx); /// Exit a parse tree produced by [BBCodeParser.bgm]. /// [ctx] the parse tree void exitBgm(BgmContext ctx); /// Enter a parse tree produced by [BBCodeParser.sticker]. /// [ctx] the parse tree void enterSticker(StickerContext ctx); /// Exit a parse tree produced by [BBCodeParser.sticker]. /// [ctx] the parse tree void exitSticker(StickerContext ctx); } ================================================ FILE: lib/bbcode/generated/BBCodeParser.dart ================================================ import 'package:antlr4/antlr4.dart'; import 'BBCodeListener.dart'; const int RULE_document = 0, RULE_element = 1, RULE_tag = 2, RULE_plain = 3, RULE_bgm = 4, RULE_sticker = 5; class BBCodeParser extends Parser { static final checkVersion = () => RuntimeMetaData.checkVersion('4.13.2', RuntimeMetaData.VERSION); static const int TOKEN_EOF = IntStream.EOF; static final List _decisionToDFA = List.generate( _ATN.numberOfDecisions, (i) => DFA(_ATN.getDecisionState(i), i)); static final PredictionContextCache _sharedContextCache = PredictionContextCache(); static const int TOKEN_T__0 = 1, TOKEN_T__1 = 2, TOKEN_T__2 = 3, TOKEN_T__3 = 4, TOKEN_T__4 = 5, TOKEN_T__5 = 6, TOKEN_T__6 = 7, TOKEN_T__7 = 8, TOKEN_T__8 = 9, TOKEN_T__9 = 10, TOKEN_T__10 = 11, TOKEN_T__11 = 12, TOKEN_T__12 = 13, TOKEN_T__13 = 14, TOKEN_T__14 = 15, TOKEN_T__15 = 16, TOKEN_T__16 = 17, TOKEN_T__17 = 18, TOKEN_T__18 = 19, TOKEN_T__19 = 20, TOKEN_T__20 = 21, TOKEN_T__21 = 22, TOKEN_T__22 = 23, TOKEN_T__23 = 24, TOKEN_T__24 = 25, TOKEN_T__25 = 26, TOKEN_T__26 = 27, TOKEN_STRING = 28; @override final List ruleNames = [ 'document', 'element', 'tag', 'plain', 'bgm', 'sticker' ]; static final List _LITERAL_NAMES = [ null, "'['", "'='", "']'", "'[/'", "'/'", "'('", "')'", "'[\\u6765\\u81EABangumi for android]'", "'[\\u6765\\u81EABangumi for iOS]'", "'(bgm'", "'(BGM'", "'(=A=)'", "'(=w=)'", "'(-w=)'", "'(S_S)'", "'(=v=)'", "'(@_@)'", "'(=W=)'", "'(TAT)'", "'(T_T)'", "'(='=)'", "'(=3=)'", "'(= =')'", "'(=///=)'", "'(=.,=)'", "'(:P)'", "'(LOL)'" ]; static final List _SYMBOLIC_NAMES = [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "STRING" ]; static final Vocabulary VOCABULARY = VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @override Vocabulary get vocabulary { return VOCABULARY; } @override String get grammarFileName => 'BBCode.g4'; @override List get serializedATN => _serializedATN; @override ATN getATN() { return _ATN; } BBCodeParser(TokenStream input) : super(input) { interpreter = ParserATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache); } DocumentContext document() { dynamic _localctx = DocumentContext(context, state); enterRule(_localctx, 0, RULE_document); int _la; try { enterOuterAlt(_localctx, 1); state = 15; errorHandler.sync(this); _la = tokenStream.LA(1)!; while ((((_la) & ~0x3f) == 0 && ((1 << _la) & 536870894) != 0)) { state = 12; element(); state = 17; errorHandler.sync(this); _la = tokenStream.LA(1)!; } state = 18; match(TOKEN_EOF); } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } ElementContext element() { dynamic _localctx = ElementContext(context, state); enterRule(_localctx, 2, RULE_element); try { state = 24; errorHandler.sync(this); switch (interpreter!.adaptivePredict(tokenStream, 1, context)) { case 1: enterOuterAlt(_localctx, 1); state = 20; tag(); break; case 2: enterOuterAlt(_localctx, 2); state = 21; plain(); break; case 3: enterOuterAlt(_localctx, 3); state = 22; bgm(); break; case 4: enterOuterAlt(_localctx, 4); state = 23; sticker(); break; } } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } TagContext tag() { dynamic _localctx = TagContext(context, state); enterRule(_localctx, 4, RULE_tag); int _la; try { enterOuterAlt(_localctx, 1); state = 26; match(TOKEN_T__0); state = 27; _localctx.tagName = match(TOKEN_STRING); state = 30; errorHandler.sync(this); _la = tokenStream.LA(1)!; if (_la == TOKEN_T__1) { state = 28; match(TOKEN_T__1); state = 29; _localctx.attr = match(TOKEN_STRING); } state = 32; match(TOKEN_T__2); state = 36; errorHandler.sync(this); _la = tokenStream.LA(1)!; while ((((_la) & ~0x3f) == 0 && ((1 << _la) & 536870894) != 0)) { state = 33; _localctx.content = element(); state = 38; errorHandler.sync(this); _la = tokenStream.LA(1)!; } state = 39; match(TOKEN_T__3); state = 40; match(TOKEN_STRING); state = 41; match(TOKEN_T__2); } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } PlainContext plain() { dynamic _localctx = PlainContext(context, state); enterRule(_localctx, 6, RULE_plain); int _la; try { int _alt; state = 50; errorHandler.sync(this); switch (tokenStream.LA(1)!) { case TOKEN_T__0: case TOKEN_T__1: case TOKEN_T__2: case TOKEN_T__4: case TOKEN_T__5: case TOKEN_T__6: case TOKEN_STRING: enterOuterAlt(_localctx, 1); state = 44; errorHandler.sync(this); _alt = 1; do { switch (_alt) { case 1: state = 43; _la = tokenStream.LA(1)!; if (!((((_la) & ~0x3f) == 0 && ((1 << _la) & 268435694) != 0))) { errorHandler.recoverInline(this); } else { if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true; errorHandler.reportMatch(this); consume(); } break; default: throw NoViableAltException(this); } state = 46; errorHandler.sync(this); _alt = interpreter!.adaptivePredict(tokenStream, 4, context); } while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER); break; case TOKEN_T__7: enterOuterAlt(_localctx, 2); state = 48; match(TOKEN_T__7); break; case TOKEN_T__8: enterOuterAlt(_localctx, 3); state = 49; match(TOKEN_T__8); break; default: throw NoViableAltException(this); } } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } BgmContext bgm() { dynamic _localctx = BgmContext(context, state); enterRule(_localctx, 8, RULE_bgm); int _la; try { enterOuterAlt(_localctx, 1); state = 52; _la = tokenStream.LA(1)!; if (!(_la == TOKEN_T__9 || _la == TOKEN_T__10)) { errorHandler.recoverInline(this); } else { if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true; errorHandler.reportMatch(this); consume(); } state = 53; _localctx.id = match(TOKEN_STRING); state = 54; match(TOKEN_T__6); } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } StickerContext sticker() { dynamic _localctx = StickerContext(context, state); enterRule(_localctx, 10, RULE_sticker); int _la; try { enterOuterAlt(_localctx, 1); state = 56; _la = tokenStream.LA(1)!; if (!((((_la) & ~0x3f) == 0 && ((1 << _la) & 268431360) != 0))) { errorHandler.recoverInline(this); } else { if ( tokenStream.LA(1)! == IntStream.EOF ) matchedEOF = true; errorHandler.reportMatch(this); consume(); } } on RecognitionException catch (re) { _localctx.exception = re; errorHandler.reportError(this, re); errorHandler.recover(this, re); } finally { exitRule(); } return _localctx; } static const List _serializedATN = [ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 30,36,46,50 ]; static final ATN _ATN = ATNDeserializer().deserialize(_serializedATN); } class DocumentContext extends ParserRuleContext { TerminalNode? EOF() => getToken(BBCodeParser.TOKEN_EOF, 0); List elements() => getRuleContexts(); ElementContext? element(int i) => getRuleContext(i); DocumentContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_document; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterDocument(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitDocument(this); } } class ElementContext extends ParserRuleContext { TagContext? tag() => getRuleContext(0); PlainContext? plain() => getRuleContext(0); BgmContext? bgm() => getRuleContext(0); StickerContext? sticker() => getRuleContext(0); ElementContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_element; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterElement(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitElement(this); } } class TagContext extends ParserRuleContext { Token? tagName; Token? attr; ElementContext? content; List STRINGs() => getTokens(BBCodeParser.TOKEN_STRING); TerminalNode? STRING(int i) => getToken(BBCodeParser.TOKEN_STRING, i); List elements() => getRuleContexts(); ElementContext? element(int i) => getRuleContext(i); TagContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_tag; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterTag(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitTag(this); } } class PlainContext extends ParserRuleContext { List STRINGs() => getTokens(BBCodeParser.TOKEN_STRING); TerminalNode? STRING(int i) => getToken(BBCodeParser.TOKEN_STRING, i); PlainContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_plain; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterPlain(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitPlain(this); } } class BgmContext extends ParserRuleContext { Token? id; TerminalNode? STRING() => getToken(BBCodeParser.TOKEN_STRING, 0); BgmContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_bgm; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterBgm(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitBgm(this); } } class StickerContext extends ParserRuleContext { StickerContext([ParserRuleContext? parent, int? invokingState]) : super(parent, invokingState); @override int get ruleIndex => RULE_sticker; @override void enterRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.enterSticker(this); } @override void exitRule(ParseTreeListener listener) { if (listener is BBCodeListener) listener.exitSticker(this); } } ================================================ FILE: lib/bean/appbar/drag_to_move_bar.dart ================================================ import 'package:kazumi/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; /// A widget for drag to move window. /// /// When you have hidden the title bar, you can add this widget to move the window position. /// /// {@tool snippet} /// /// The sample creates a red box, drag the box to move the window. /// /// ```dart /// DragToMoveArea( /// child: Container( /// width: 300, /// height: 32, /// color: Colors.red, /// ), /// ) /// ``` /// {@end-tool} class DragToMoveArea extends StatelessWidget { const DragToMoveArea({ super.key, required this.child, }); final Widget child; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.translucent, onPanStart: (_) => (Utils.isDesktop()) ? windowManager.startDragging() : null, child: child, ); } } ================================================ FILE: lib/bean/appbar/safe_mediaquery_warpper.dart ================================================ import 'package:flutter/material.dart'; /// workaround for padding check error on Xiaomi HyperOS devices /// caused by flutter/flutter#161086 /// this is a temporary solution, will be removed in the future class SafeMediaQueryWrapper extends StatelessWidget { final Widget child; final double defaultTopPadding; final double defaultBottomPadding; const SafeMediaQueryWrapper({ super.key, required this.child, this.defaultTopPadding = 25, this.defaultBottomPadding = 0, }); @override Widget build(BuildContext context) { final mediaQuery = MediaQuery.of(context); final viewPadding = mediaQuery.viewPadding; final isPaddingCheckError = viewPadding.top < 0 || viewPadding.top > 50; if (!isPaddingCheckError) { return child; } return MediaQuery( data: mediaQuery.copyWith( viewPadding: EdgeInsets.only( top: defaultTopPadding, bottom: defaultBottomPadding, left: viewPadding.left, right: viewPadding.right, ), padding: EdgeInsets.only( top: defaultTopPadding, bottom: defaultBottomPadding, left: mediaQuery.padding.left, right: mediaQuery.padding.right, ), ), child: child, ); } } ================================================ FILE: lib/bean/appbar/sys_app_bar.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:window_manager/window_manager.dart'; class SysAppBar extends StatelessWidget implements PreferredSizeWidget { final double? toolbarHeight; final Widget? title; final Color? backgroundColor; final double? elevation; final ShapeBorder? shape; final List? actions; final Widget? leading; final double? leadingWidth; final PreferredSizeWidget? bottom; final bool needTopOffset; const SysAppBar( {super.key, this.toolbarHeight, this.title, this.backgroundColor, this.elevation, this.shape, this.actions, this.leading, this.leadingWidth, this.bottom, this.needTopOffset = true}); bool showWindowButton() { return GStorage.setting .get(SettingBoxKey.showWindowButton, defaultValue: false); } @override Widget build(BuildContext context) { List acs = []; if (actions != null) { acs.addAll(actions!); } if (Utils.isDesktop()) { // acs.add(IconButton(onPressed: () => windowManager.minimize(), icon: const Icon(Icons.minimize))); if (!showWindowButton()) { acs.add(CloseButton(onPressed: () => windowManager.close())); } acs.add(const SizedBox(width: 8)); } return GestureDetector( onPanStart: (_) => (Utils.isDesktop()) ? windowManager.startDragging() : null, child: AppBar( toolbarHeight: preferredSize.height, scrolledUnderElevation: 0.0, title: title != null ? EmbeddedNativeControlArea( requireOffset: needTopOffset, child: title!, ) : null, centerTitle: Platform.isIOS ? true : false, actions: acs.map((e) { return EmbeddedNativeControlArea( requireOffset: needTopOffset, child: e, ); }).toList(), leading: leading != null ? EmbeddedNativeControlArea( requireOffset: needTopOffset, child: leading!, ) : Navigator.canPop(context) ? EmbeddedNativeControlArea( requireOffset: needTopOffset, child: IconButton( onPressed: () { Navigator.maybePop(context); }, icon: Icon(Icons.arrow_back), ), ) : null, leadingWidth: leadingWidth, backgroundColor: backgroundColor, elevation: elevation, shape: shape, bottom: bottom, automaticallyImplyLeading: false, systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Theme.of(context).brightness == Brightness.light ? Brightness.dark : Brightness.light, systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent, ), ), ); } @override Size get preferredSize { // macOS needs to add 22(macOS title bar height) // to default toolbar height to build appbar like normal if (Platform.isMacOS && needTopOffset && showWindowButton()) { if (toolbarHeight != null) { return Size.fromHeight(toolbarHeight! + 22); } else { return const Size.fromHeight(kToolbarHeight + 22); } } else { return Size.fromHeight(toolbarHeight ?? kToolbarHeight); } } } ================================================ FILE: lib/bean/card/bangumi_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/utils/utils.dart'; // 视频卡片 - 垂直布局 class BangumiCardV extends StatelessWidget { const BangumiCardV({ super.key, required this.bangumiItem, this.canTap = true, this.enableHero = true, }); final BangumiItem bangumiItem; final bool canTap; final bool enableHero; @override Widget build(BuildContext context) { return Card( elevation: 0, clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, child: GestureDetector( child: InkWell( onTap: () { if (!canTap) { KazumiDialog.showToast( message: '编辑模式', ); return; } Modular.to.pushNamed('/info/', arguments: bangumiItem); }, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AspectRatio( aspectRatio: 0.65, child: LayoutBuilder(builder: (context, boxConstraints) { final double maxWidth = boxConstraints.maxWidth; final double maxHeight = boxConstraints.maxHeight; return enableHero ? Hero( transitionOnUserGestures: true, tag: bangumiItem.id, child: NetworkImgLayer( src: bangumiItem.images['large'] ?? '', width: maxWidth, height: maxHeight, ), ) : NetworkImgLayer( src: bangumiItem.images['large'] ?? '', width: maxWidth, height: maxHeight, ); }), ), BangumiContent(bangumiItem: bangumiItem) ], ), ), ), ); } } class BangumiContent extends StatelessWidget { const BangumiContent({super.key, required this.bangumiItem}); final BangumiItem bangumiItem; @override Widget build(BuildContext context) { final ts = MediaQuery.textScalerOf(context); final int maxTextLines = Utils.isDesktop() ? 3 : (Utils.isTablet() && MediaQuery.of(context).orientation == Orientation.landscape) ? 3 : 2; return Expanded( child: Padding( // 多列 padding: const EdgeInsets.fromLTRB(5, 3, 5, 1), // 单列 // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), child: Text( bangumiItem.nameCn, textAlign: TextAlign.start, style: const TextStyle( fontWeight: FontWeight.w500, letterSpacing: 0.3, ), textScaler: ts.clamp(maxScaleFactor: 1.1), maxLines: maxTextLines, overflow: TextOverflow.ellipsis, ), ), ); } } ================================================ FILE: lib/bean/card/bangumi_history_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/utils.dart'; // 视频历史记录卡片 - 水平布局 class BangumiHistoryCardV extends StatefulWidget { const BangumiHistoryCardV({ super.key, required this.historyItem, this.showDelete = true, this.cardHeight = 120, this.cardWidth, }); final History historyItem; final bool showDelete; final double cardHeight; final double? cardWidth; @override State createState() => _BangumiHistoryCardVState(); } class _BangumiHistoryCardVState extends State { final VideoPageController videoPageController = Modular.get(); final PluginsController pluginsController = Modular.get(); final HistoryController historyController = Modular.get(); Widget propertyChip({ required String title, required String value, bool showTitle = false, }) { final message = '$title: $value'; return Chip( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(32)), backgroundColor: Theme.of(context).colorScheme.secondaryContainer, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 2), side: BorderSide.none, label: Text( showTitle ? message : value, style: Theme.of(context).textTheme.labelSmall, overflow: TextOverflow.ellipsis, maxLines: 1, ), ); } Widget buildImage( BuildContext context, String imageUrl, double width, double height) { final borderRadius = BorderRadius.circular(16); Widget img = NetworkImgLayer( src: imageUrl, width: width, height: height, ); img = ClipRRect( borderRadius: borderRadius, child: img, ); return img; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final double borderRadius = 18; final double imageWidth = widget.cardHeight * 0.7; return Card( elevation: 1, margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius), ), clipBehavior: Clip.antiAlias, color: theme.colorScheme.surface, child: InkWell( onTap: () async { if (widget.showDelete) { KazumiDialog.showToast( message: '编辑模式', ); return; } KazumiDialog.showLoading( msg: '获取中', barrierDismissible: Utils.isDesktop(), onDismiss: () { videoPageController.cancelQueryRoads(); }, ); bool flag = false; for (Plugin plugin in pluginsController.pluginList) { if (plugin.name == widget.historyItem.adapterName) { videoPageController.currentPlugin = plugin; flag = true; break; } } if (!flag) { KazumiDialog.dismiss(); KazumiDialog.showToast(message: '未找到关联番剧源'); return; } videoPageController.bangumiItem = widget.historyItem.bangumiItem; videoPageController.title = widget.historyItem.bangumiItem.nameCn == '' ? widget.historyItem.bangumiItem.name : widget.historyItem.bangumiItem.nameCn; videoPageController.src = widget.historyItem.lastSrc; try { await videoPageController.queryRoads(widget.historyItem.lastSrc, videoPageController.currentPlugin.name); KazumiDialog.dismiss(); Modular.to.pushNamed('/video/'); } catch (_) { KazumiLogger().w("QueryManager: failed to query roads"); KazumiDialog.dismiss(); } }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ buildImage( context, widget.historyItem.bangumiItem.images['large'] ?? '', imageWidth, widget.cardHeight), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 6), Text( widget.historyItem.bangumiItem.nameCn == '' ? widget.historyItem.bangumiItem.name : widget.historyItem.bangumiItem.nameCn, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), const SizedBox(height: 12), Wrap( spacing: 4, runSpacing: 4, children: [ propertyChip( title: '来源', value: widget.historyItem.adapterName, showTitle: true, ), propertyChip( title: '看到', value: widget.historyItem.lastWatchEpisodeName.isEmpty ? '第${widget.historyItem.lastWatchEpisode}话' : widget.historyItem.lastWatchEpisodeName, showTitle: true, ), ], ) ], ), ), ), Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (!widget.showDelete) ...[ CollectButton( onClose: () { FocusScope.of(context).unfocus(); }, bangumiItem: widget.historyItem.bangumiItem, color: Theme.of(context).colorScheme.onSecondaryContainer, ), IconButton( icon: Icon( Icons.open_in_new, color: Theme.of(context).colorScheme.onSecondaryContainer, ), tooltip: '番剧详情', onPressed: () { Modular.to.pushNamed( '/info/', arguments: widget.historyItem.bangumiItem, ); }, ), ], if (widget.showDelete) IconButton( icon: Icon( Icons.delete, color: Theme.of(context).colorScheme.onSecondaryContainer, ), onPressed: () { historyController.deleteHistory(widget.historyItem); }, ), ], ), ], ), ), ); } } ================================================ FILE: lib/bean/card/bangumi_info_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:skeletonizer/skeletonizer.dart'; // 视频卡片 - 水平布局 class BangumiInfoCardV extends StatefulWidget { const BangumiInfoCardV({ super.key, required this.bangumiItem, required this.isLoading, required this.showRating, }); final BangumiItem bangumiItem; final bool isLoading; final bool showRating; @override State createState() => _BangumiInfoCardVState(); } class _BangumiInfoCardVState extends State { int touchedIndex = -1; Widget get voteBarChart { return Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ' 评分透视:', ), SizedBox(height: 16), AspectRatio( aspectRatio: 2, child: BarChart( duration: Duration(milliseconds: 80), BarChartData( // alignment: BarChartAlignment.spaceEvenly, borderData: FlBorderData(show: false), gridData: FlGridData(show: false), barTouchData: BarTouchData( touchCallback: (FlTouchEvent event, barTouchResponse) { setState(() { if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) { touchedIndex = -1; return; } touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; }); }, touchTooltipData: BarTouchTooltipData( getTooltipColor: (_) => Theme.of(context).colorScheme.inverseSurface, getTooltipItem: (group, groupIndex, rod, rodIndex) { var percentage = widget.bangumiItem.votesCount[groupIndex] / widget.bangumiItem.votes * 100; return BarTooltipItem( '${percentage.toStringAsFixed(2)}% (${widget.bangumiItem.votesCount[groupIndex]}人)', TextStyle( color: Theme.of(context).colorScheme.onInverseSurface), ); }, ), ), barGroups: List.generate( 10, (i) => BarChartGroupData( x: i + 1, barRods: [ BarChartRodData( toY: widget.bangumiItem.votesCount[i].toDouble(), color: touchedIndex == i ? Theme.of(context).colorScheme.primary : Theme.of(context).disabledColor, width: 20, borderRadius: BorderRadius.vertical(top: Radius.circular(5)), ) ], // showingTooltipIndicators: [0], ), ), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) => SideTitleWidget( meta: meta, space: 10, child: Text(value.toInt().toString()), ), ), ), topTitles: const AxisTitles(), leftTitles: const AxisTitles(), rightTitles: const AxisTitles(), ), ), ), ), ], ), ); } @override Widget build(BuildContext context) { return Container( height: 300, constraints: BoxConstraints(maxWidth: 950), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.bangumiItem.nameCn == '' ? widget.bangumiItem.name : (widget.bangumiItem.nameCn), maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.headlineSmall, ), SizedBox(height: 16), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: AspectRatio( aspectRatio: 0.65, child: LayoutBuilder(builder: (context, boxConstraints) { final double maxWidth = boxConstraints.maxWidth; final double maxHeight = boxConstraints.maxHeight; return Hero( transitionOnUserGestures: true, tag: widget.bangumiItem.id, child: NetworkImgLayer( src: widget.bangumiItem.images['large'] ?? '', width: maxWidth, height: maxHeight, fadeInDuration: const Duration(milliseconds: 0), fadeOutDuration: const Duration(milliseconds: 0), ), ); }), ), ), SizedBox(width: 16), Flexible( child: Skeletonizer( enabled: widget.isLoading, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '放送开始:', ), Text( widget.bangumiItem.airDate == '' ? '2000-11-11' // Skeleton Loader 占位符 : widget.bangumiItem.airDate, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), SizedBox(height: 8), Text( widget.showRating ? '${widget.bangumiItem.votes} 人评分:' : '*** 人评分:', ), if (widget.isLoading) // Skeleton Loader 占位符 Text( '10.0 ********', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), if (!widget.isLoading) Row( children: [ Text( widget.showRating ? '${widget.bangumiItem.ratingScore}' : '***', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(width: 8), RatingBarIndicator( itemCount: 5, rating: widget.showRating ? widget.bangumiItem.ratingScore .toDouble() / 2 : 0, itemBuilder: (context, index) => Icon( Icons.star_rounded, color: Theme.of(context).colorScheme.primary, ), itemSize: 20.0, ), ], ), SizedBox(height: 8), Text( 'Bangumi Ranked:', ), Text( widget.showRating ? '#${widget.bangumiItem.rank}' : '***', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), ], ), SizedBox( width: 120, height: 40, child: CollectButton.extend( bangumiItem: widget.bangumiItem, ), ), ], ), ), ), if (widget.showRating && MediaQuery.sizeOf(context).width >= LayoutBreakpoint.compact['width']! && !widget.isLoading) voteBarChart, ], ), ), ], ), ); } } ================================================ FILE: lib/bean/card/bangumi_timeline_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; /// 时间线番剧卡片 class BangumiTimelineCard extends StatelessWidget { const BangumiTimelineCard({ super.key, required this.bangumiItem, required this.showRating, this.onTap, this.cardHeight = 120, this.cardWidth, this.enableHero = true, }); final BangumiItem bangumiItem; final bool showRating; final VoidCallback? onTap; final bool enableHero; final double cardHeight; final double? cardWidth; @override Widget build(BuildContext context) { final isDesktop = Utils.isDesktop(); final isTablet = Utils.isTablet(); final theme = Theme.of(context); final textScaler = MediaQuery.textScalerOf(context); final double imageWidth = cardHeight * 0.7; final double borderRadius = 18; return Card( elevation: 1, margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius), ), clipBehavior: Clip.antiAlias, color: theme.colorScheme.surface, child: InkWell( borderRadius: BorderRadius.circular(borderRadius), onTap: onTap ?? () { Modular.to.pushNamed('/info/', arguments: bangumiItem); }, child: SizedBox( height: cardHeight, width: cardWidth, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ buildImage(context, bangumiItem.images['large'] ?? '', imageWidth, cardHeight), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), child: buildInfo(context, textScaler, isDesktop, isTablet), ), ), ], ), ), ), ); } Widget buildImage(BuildContext context, String imageUrl, double width, double height) { final borderRadius = BorderRadius.circular(16); Widget img = NetworkImgLayer( src: imageUrl, width: width, height: height, ); if (enableHero) { img = Hero( tag: bangumiItem.id, transitionOnUserGestures: true, child: ClipRRect( borderRadius: borderRadius, child: img, ), ); } else { img = ClipRRect( borderRadius: borderRadius, child: img, ); } return img; } Widget buildInfo(BuildContext context, TextScaler textScaler, bool isDesktop, bool isTablet) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final nameStyle = theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600); final subStyle = theme.textTheme.bodySmall ?.copyWith(color: colorScheme.onSurfaceVariant); final infoStyle = theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500); final maxLines = isDesktop ? 2 : 1; final double spacing = isDesktop ? 8 : 4; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题 Text( bangumiItem.nameCn.isNotEmpty ? bangumiItem.nameCn : bangumiItem.name, style: nameStyle, maxLines: maxLines, overflow: TextOverflow.ellipsis, textScaler: textScaler.clamp(maxScaleFactor: 1.1), ), SizedBox(height: spacing), // 简介 if (bangumiItem.summary.isNotEmpty || bangumiItem.info.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 2), child: DecoratedBox( decoration: BoxDecoration( color: colorScheme.primaryContainer.withAlpha((255 * 0.10).round()), borderRadius: BorderRadius.circular(6), ), child: Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( bangumiItem.info.isNotEmpty ? bangumiItem.info : bangumiItem.summary, style: theme.textTheme.labelMedium?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w500, ), maxLines: 3, overflow: TextOverflow.ellipsis, textScaler: textScaler.clamp(maxScaleFactor: 1.0), ), ), ), ), const Spacer(), Row( children: [ if (showRating ? bangumiItem.ratingScore > 0 : true) Row( children: [ Icon(Icons.star_rounded, size: 15, color: colorScheme.primary), const SizedBox(width: 2), Text( showRating ? bangumiItem.ratingScore.toStringAsFixed(1) : '***', style: infoStyle), ], ), if (showRating ? bangumiItem.rank > 0 : true) Padding( padding: const EdgeInsets.only(left: 8), child: Row( children: [ Icon(Icons.leaderboard, size: 15, color: colorScheme.tertiary), const SizedBox(width: 2), Text( showRating ? 'Rank ${bangumiItem.rank}' : 'Rank ***', style: infoStyle), ], ), ), const Spacer(), if (showRating ? bangumiItem.votes > 0 : true) Text( showRating ? '评分人数: ${bangumiItem.votes}' : '评分人数: ***', style: subStyle), ], ), ], ); } } ================================================ FILE: lib/bean/card/character_card.dart ================================================ import 'package:kazumi/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:kazumi/modules/characters/character_item.dart'; import 'package:kazumi/pages/info/character_page.dart'; class CharacterCard extends StatelessWidget { const CharacterCard({ super.key, required this.characterItem, }); final CharacterItem characterItem; @override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( backgroundImage: characterItem.avator.grid.isEmpty ? NetworkImage('https://bangumi.tv/img/info_only.png') : NetworkImage(characterItem.avator.grid), ), title: Text( characterItem.name, overflow: TextOverflow.ellipsis, maxLines: 1, ), subtitle: characterItem.actorList.isNotEmpty ? Text(characterItem.actorList[0].name) : null, trailing: Text(characterItem.relation), onTap: () { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return CharacterPage(characterID: characterItem.id); }); }, ); } } ================================================ FILE: lib/bean/card/character_comments_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bbcode/bbcode_widget.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/utils/utils.dart'; class CharacterCommentsCard extends StatelessWidget { const CharacterCommentsCard({ super.key, required this.commentItem, }); final CharacterCommentItem commentItem; @override Widget build(BuildContext context) { return Card( // color: Theme.of(context).colorScheme.secondaryContainer, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundImage: NetworkImage(commentItem.comment.user.avatar.large), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(commentItem.comment.user.nickname), Text(Utils.dateFormat(commentItem.comment.createdAt)), ], ), ], ), const SizedBox(height: 8), BBCodeWidget(bbcode: commentItem.comment.comment), if (commentItem.replies.isNotEmpty) ListView.builder( // Don't know why but some device has bottom padding, // needs to set to 0 manually. padding: const EdgeInsets.only(bottom: 0), physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: commentItem.replies.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(left: 48), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Divider( color: Theme.of(context).dividerColor.withAlpha(60), ), Row( children: [ CircleAvatar( backgroundImage: NetworkImage( commentItem.replies[index].user.avatar.large), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(commentItem.replies[index].user.nickname), Text( Utils.dateFormat( commentItem.replies[index].createdAt), ), ], ), ], ), const SizedBox(height: 8), BBCodeWidget( bbcode: commentItem.replies[index].comment), ], ), ); }, ), ], ), ), ); } } ================================================ FILE: lib/bean/card/comments_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:skeletonizer/skeletonizer.dart'; class CommentsCard extends StatelessWidget { CommentsCard({ super.key, required this.commentItem, }) { isBone = false; } CommentsCard.bone({ super.key, }) { isBone = true; commentItem = null; } late final CommentItem? commentItem; late final bool isBone; @override Widget build(BuildContext context) { if (isBone) { return Skeletonizer.zone( enabled: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Bone.circle(size: 36), const SizedBox(width: 8), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Bone.text(width: 80), SizedBox(height: 8), Bone.text(width: 60), ], ), ], ), SizedBox(height: 8), const Bone.multiText(lines: 2), Divider(thickness: 0.5, indent: 10, endIndent: 10), ], ), ); } return SelectionArea( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( backgroundImage: NetworkImage(commentItem!.user.avatar.large), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(commentItem!.user.nickname), Text(Utils.dateFormat(commentItem!.comment.updatedAt)), ], ), Expanded(child: Container(height: 10)), RatingBarIndicator( itemCount: 5, rating: commentItem!.comment.rate.toDouble() / 2, itemBuilder: (context, index) => const Icon( Icons.star_rounded, ), itemSize: 20.0, ), ], ), const SizedBox(height: 8), Text(commentItem!.comment.comment), ], ), ), ); } } ================================================ FILE: lib/bean/card/episode_comments_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bbcode/bbcode_widget.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/utils/utils.dart'; class EpisodeCommentsCard extends StatelessWidget { const EpisodeCommentsCard({ super.key, required this.commentItem, }); final EpisodeCommentItem commentItem; @override Widget build(BuildContext context) { // 对 用户评论 做判空操作,如果为空则显示“用户已删除” String userComment = commentItem.comment.comment; if (userComment.isEmpty) { userComment = "<用户已删除>"; } return Card( // color: Theme.of(context).colorScheme.secondaryContainer, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundImage: NetworkImage(commentItem.comment.user.avatar.large), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(commentItem.comment.user.nickname), Text(Utils.dateFormat(commentItem.comment.createdAt)), ], ), ], ), const SizedBox(height: 8), BBCodeWidget(bbcode: userComment), if (commentItem.replies.isNotEmpty) ListView.builder( // Don't know why but ohos has bottom padding, // needs to set to 0 manually. padding: const EdgeInsets.only(bottom: 0), physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: commentItem.replies.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(left: 48), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Divider( color: Theme.of(context).dividerColor.withAlpha(60), ), Row( children: [ CircleAvatar( backgroundImage: NetworkImage( commentItem.replies[index].user.avatar.large), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(commentItem.replies[index].user.nickname), Text( Utils.dateFormat( commentItem.replies[index].createdAt), ), ], ), ], ), const SizedBox(height: 8), BBCodeWidget( bbcode: commentItem.replies[index].comment), ], ), ); }, ), ], ), ), ); } } ================================================ FILE: lib/bean/card/network_img_layer.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/extension.dart'; import 'package:kazumi/utils/logger.dart'; class NetworkImgLayer extends StatelessWidget { const NetworkImgLayer({ super.key, this.src, required this.width, required this.height, this.type, this.fadeOutDuration, this.fadeInDuration, this.quality, this.origAspectRatio, }); final String? src; final double width; final double height; final String? type; final Duration? fadeOutDuration; final Duration? fadeInDuration; final int? quality; final double? origAspectRatio; @override Widget build(BuildContext context) { final String imageUrl = src ?? ''; //// We need this to shink memory usage int? memCacheWidth, memCacheHeight; double aspectRatio = (width / height).toDouble(); void setMemCacheSizes() { if (aspectRatio > 1) { memCacheHeight = height.cacheSize(context); } else if (aspectRatio < 1) { memCacheWidth = width.cacheSize(context); } else { if (origAspectRatio != null && origAspectRatio! > 1) { memCacheWidth = width.cacheSize(context); } else if (origAspectRatio != null && origAspectRatio! < 1) { memCacheHeight = height.cacheSize(context); } else { memCacheWidth = width.cacheSize(context); memCacheHeight = height.cacheSize(context); } } } setMemCacheSizes(); if (memCacheWidth == null && memCacheHeight == null) { memCacheWidth = width.toInt(); } return src != '' && src != null ? ClipRRect( clipBehavior: Clip.antiAlias, borderRadius: BorderRadius.circular( type == 'avatar' ? 50 : type == 'emote' ? 0 : StyleString.imgRadius.x, ), child: CachedNetworkImage( imageUrl: imageUrl, width: width, height: height, memCacheWidth: memCacheWidth, memCacheHeight: memCacheHeight, fit: BoxFit.cover, fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120), fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120), filterQuality: FilterQuality.high, errorListener: (e) { KazumiLogger().w("NetworkImage: network image load error", error: e); }, errorWidget: (BuildContext context, String url, Object error) => placeholder(context), placeholder: (BuildContext context, String url) => placeholder(context), )) : placeholder(context); } Widget placeholder(BuildContext context) { return Container( width: width, height: height, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Theme.of(context).colorScheme.onInverseSurface.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(type == 'avatar' ? 50 : type == 'emote' ? 0 : StyleString.imgRadius.x), ), child: type == 'bg' ? const SizedBox() : Center( child: Image.asset( type == 'avatar' ? 'assets/images/noface.jpeg' : 'assets/images/loading.png', width: width, height: height, cacheWidth: width.cacheSize(context), cacheHeight: height.cacheSize(context), ), ), ); } } ================================================ FILE: lib/bean/card/palette_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; class PaletteCard extends StatefulWidget { final Color color; final bool selected; const PaletteCard({ super.key, required this.color, required this.selected, }); @override State createState() => _PaletteCardState(); } class _PaletteCardState extends State { @override Widget build(BuildContext context) { final Hct hct = Hct.fromInt(widget.color.value); final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt()); final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt()); final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt()); final checkbox = Color(Hct.from(hct.hue, 30.0, 40.0).toInt()); return SizedBox( width: 70, height: 70, child: Stack( children: [ Card( elevation: 0, child: Container( padding: const EdgeInsets.all(10), child: ClipOval( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Container( color: primary, ), ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Container( color: tertiary, ), ), Expanded( child: Container( color: primaryContainer, ), ), ], ), ), ], ), ), ), ), if (widget.selected) Center( child: Container( width: 25, height: 25, decoration: BoxDecoration( color: checkbox, shape: BoxShape.circle, ), child: Icon( Icons.check_rounded, color: Theme.of(context).colorScheme.surfaceContainerLow, size: 12, ), ), ), ], ), ); } } ================================================ FILE: lib/bean/card/staff_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/modules/staff/staff_item.dart'; class StaffCard extends StatelessWidget { const StaffCard({ super.key, required this.staffFullItem, }); final StaffFullItem staffFullItem; @override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( backgroundImage: staffFullItem.staff.images?.grid == null ? NetworkImage('https://bangumi.tv/img/info_only.png') : NetworkImage(staffFullItem.staff.images!.grid), ), title: Text( staffFullItem.staff.name, overflow: TextOverflow.ellipsis, maxLines: 1, ), subtitle: staffFullItem.staff.nameCN.isNotEmpty ? Text(staffFullItem.staff.nameCN) : null, trailing: Text(staffFullItem.positions.isNotEmpty ? (staffFullItem.positions[0].type.cn) : ''), ); } } ================================================ FILE: lib/bean/dialog/dialog_helper.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:kazumi/utils/constants.dart'; // A simple dialog helper class to show dialogs and toasts based on flutter native implementation (replace flutter_smart_dialog) // flutter_smart_dialog use overlays and self-managed route stack to show dialogs. // It's powerful but can't behave like the default showDialog, e.g. the lack of mask animation. the lack of snackbar. // Use the implementation should be careful, because shared route stack with the whole app, it may cause some unexpected behaviors. // Don't use it in double PopScope widget. class KazumiDialog { /// The global observer that tracks contexts across the application static final KazumiDialogObserver observer = KazumiDialogObserver(); KazumiDialog._internal(); static Future show({ BuildContext? context, bool? clickMaskDismiss, VoidCallback? onDismiss, required WidgetBuilder builder, }) async { final ctx = context ?? observer.currentContext; if (ctx != null && ctx.mounted) { try { final result = await showDialog( context: ctx, barrierDismissible: clickMaskDismiss ?? true, builder: builder, routeSettings: const RouteSettings(name: 'KazumiDialog'), ); onDismiss?.call(); return result; } catch (e) { debugPrint('Kazumi Dialog Error: Failed to show dialog: $e'); return null; } } else { debugPrint( 'Kazumi Dialog Error: No context available to show the dialog'); return null; } } static void showToast({ required String message, BuildContext? context, bool showActionButton = false, String? actionLabel, Function()? onActionPressed, Duration duration = const Duration(seconds: 2), }) { final ctx = context ?? observer.scaffoldContext; if (ctx != null && ctx.mounted) { try { ScaffoldMessenger.of(ctx) ..removeCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(message), behavior: SnackBarBehavior.floating, width: MediaQuery.sizeOf(ctx).width > LayoutBreakpoint.medium['width']! ? 600 : null, duration: duration, action: showActionButton ? SnackBarAction( label: actionLabel ?? 'Dismiss', onPressed: () { onActionPressed?.call(); ScaffoldMessenger.of(ctx).hideCurrentSnackBar(); }, ) : null, ), ); } catch (e) { debugPrint('Kazumi Dialog Error: Failed to show toast: $e'); } } else { debugPrint( 'Kazumi Dialog Error: No Scaffold context available to show Toast'); } } static Future showLoading({ BuildContext? context, String? msg, bool barrierDismissible = false, Function()? onDismiss, }) async { final ctx = context ?? observer.currentContext; if (ctx != null && ctx.mounted) { try { await showDialog( context: ctx, barrierDismissible: barrierDismissible, builder: (BuildContext context) { return Center( child: Card( elevation: 8.0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( msg ?? 'Loading...', style: const TextStyle(fontSize: 16), ), ], ), ), ), ); }, routeSettings: const RouteSettings(name: 'KazumiDialog'), ); onDismiss?.call(); } catch (e) { debugPrint('Kazumi Dialog Error: Failed to show loading dialog: $e'); } } else { debugPrint( 'Kazumi Dialog Error: No context available to show the loading dialog'); } } static Future showBottomSheet({ BuildContext? context, required WidgetBuilder builder, Color? backgroundColor, double? elevation, ShapeBorder? shape, Clip? clipBehavior, BoxConstraints? constraints, Color? barrierColor, bool isScrollControlled = false, bool useRootNavigator = true, bool isDismissible = true, bool enableDrag = true, RouteSettings? routeSettings, AnimationController? transitionAnimationController, Offset? anchorPoint, bool useSafeArea = false, }) async { // Use provided context first, then root context, then fallback to current context final ctx = context ?? observer.rootContext ?? observer.currentContext; if (ctx != null && ctx.mounted) { try { final result = await showModalBottomSheet( context: ctx, builder: builder, backgroundColor: backgroundColor, elevation: elevation, shape: shape, clipBehavior: clipBehavior, constraints: constraints, barrierColor: barrierColor, isScrollControlled: isScrollControlled, useRootNavigator: useRootNavigator, isDismissible: isDismissible, enableDrag: enableDrag, routeSettings: routeSettings ?? const RouteSettings(name: 'KazumiBottomSheet'), transitionAnimationController: transitionAnimationController, anchorPoint: anchorPoint, useSafeArea: useSafeArea, ); return result; } catch (e) { debugPrint('Kazumi Dialog Error: Failed to show bottom sheet: $e'); return null; } } else { debugPrint( 'Kazumi Dialog Error: No context available to show the bottom sheet'); return null; } } // 在存在返回值时弹出并附带返回值 static void dismiss({T? popWith}) { if (observer.hasKazumiDialog && observer.kazumiDialogContext != null) { try { Navigator.of(observer.kazumiDialogContext!).pop(popWith); } catch (e) { debugPrint('Kazumi Dialog Error: Failed to dismiss dialog: $e'); } } else { debugPrint('Kazumi Dialog Debug: No active KazumiDialog to dismiss'); } } /// Shows a non-dismissible timed success dialog with a linear progress /// countdown, then auto-dismisses when the countdown completes. /// /// The caller is responsible for dismissing any currently-open dialog /// BEFORE calling this method. /// /// [onComplete] is invoked inside [onDismiss] after the countdown finishes /// (or if the dialog is dismissed for any other reason), ensuring it runs /// exactly once and resources are always cleaned up. static void showTimedSuccessDialog({ required String title, required String message, required VoidCallback onComplete, Duration duration = const Duration(seconds: 3), }) { final progressNotifier = ValueNotifier(0.0); Timer? countdownTimer; final totalMs = duration.inMilliseconds; final stopwatch = Stopwatch()..start(); countdownTimer = Timer.periodic(const Duration(milliseconds: 16), (t) { final elapsed = stopwatch.elapsedMilliseconds; progressNotifier.value = (elapsed / totalMs).clamp(0.0, 1.0); if (elapsed >= totalMs) { t.cancel(); KazumiDialog.dismiss(); } }); KazumiDialog.show( clickMaskDismiss: false, onDismiss: () { countdownTimer?.cancel(); progressNotifier.dispose(); onComplete(); }, builder: (context) => Dialog( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), child: SizedBox( width: 320, child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle_rounded, size: 52, color: Theme.of(context).colorScheme.primary, ), const SizedBox(height: 16), Text( title, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 6), Text( message, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), ValueListenableBuilder( valueListenable: progressNotifier, builder: (context, value, _) => LinearProgressIndicator( value: value, borderRadius: BorderRadius.circular(4), ), ), ], ), ), ), ), ); } } /// Navigator observer to track contexts and dialog routes class KazumiDialogObserver extends NavigatorObserver { /// List of active dialog routes final List> _kazumiDialogRoutes = []; /// The most recent context from any MaterialPageRoute or PopupRoute BuildContext? _currentContext; /// The most recent context from any route containing a Scaffold BuildContext? _scaffoldContext; /// The root context of the app (for bottom sheets to cover the entire app) BuildContext? _rootContext; BuildContext? get currentContext => _currentContext; BuildContext? get scaffoldContext => _scaffoldContext ?? _currentContext; /// Get the root context for bottom sheets, fallback to scaffold context, then current context BuildContext? get rootContext => _rootContext ?? _scaffoldContext ?? _currentContext; bool get hasKazumiDialog => _kazumiDialogRoutes.isNotEmpty; BuildContext? get kazumiDialogContext => _kazumiDialogRoutes.isNotEmpty ? _kazumiDialogRoutes.last.navigator?.context : null; @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); /// workaround for #533 /// we can't remove snackbar when push a new route /// otherwise, framework will throw an exception, and can't be caught /// need other way to remove snackbar here // _removeCurrentSnackBar(previousRoute); if (_isKazumiDialogRoute(route)) { _kazumiDialogRoutes.add(route); } if (route.navigator?.context != null) { _updateContexts(route.navigator!.context, route); } } @override void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); _removeCurrentSnackBar(route); if (_isKazumiDialogRoute(route)) { _kazumiDialogRoutes.remove(route); } if (previousRoute?.navigator?.context != null) { _updateContexts(previousRoute!.navigator!.context, previousRoute); } } @override void didReplace({Route? newRoute, Route? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); if (_isKazumiDialogRoute(oldRoute!)) { _kazumiDialogRoutes.remove(oldRoute); } if (_isKazumiDialogRoute(newRoute!)) { _kazumiDialogRoutes.add(newRoute); } if (newRoute.navigator?.context != null) { _updateContexts(newRoute.navigator!.context, newRoute); } } @override void didRemove(Route route, Route? previousRoute) { super.didRemove(route, previousRoute); if (_isKazumiDialogRoute(route)) { _kazumiDialogRoutes.remove(route); } if (previousRoute?.navigator?.context != null) { _updateContexts(previousRoute!.navigator!.context, previousRoute); } } void _updateContexts(BuildContext context, Route route) { _currentContext = context; if (_hasScaffold(context)) { _scaffoldContext = context; // Always update root context with scaffold contexts to ensure we have the most recent one // This helps ensure bottom sheets appear at the app level _rootContext = context; } } bool _hasScaffold(BuildContext context) { return Scaffold.maybeOf(context) != null; } bool _isKazumiDialogRoute(Route route) { return route.settings.name == 'KazumiDialog' || route.settings.name == 'KazumiBottomSheet'; } void _removeCurrentSnackBar(Route? route) { if (route?.navigator?.context != null) { try { ScaffoldMessenger.maybeOf(route!.navigator!.context) ?.removeCurrentSnackBar(); } catch (_) {} } } } ================================================ FILE: lib/bean/settings/color_type.dart ================================================ import 'package:flutter/material.dart'; final List> colorThemeTypes = [ {'color': Colors.green, 'label': '默认'}, {'color': Colors.teal, 'label': '青色'}, {'color': Colors.blue, 'label': '蓝色'}, {'color': Colors.indigo, 'label': '靛蓝色'}, {'color': const Color(0xff6750a4), 'label': '紫罗兰色'}, {'color': Colors.pink, 'label': '粉红色'}, {'color': Colors.yellow, 'label': '黄色'}, {'color': Colors.orange, 'label': '橙色'}, {'color': Colors.deepOrange, 'label': '深橙色'}, ]; ================================================ FILE: lib/bean/settings/theme_provider.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/utils/constants.dart'; class ThemeProvider extends ChangeNotifier { ThemeMode themeMode = ThemeMode.system; bool useDynamicColor = false; late ThemeData light; late ThemeData dark; String? currentFontFamily = customAppFontFamily; void setTheme(ThemeData light, ThemeData dark, {bool notify = true}) { this.light = light; this.dark = dark; if (notify) notifyListeners(); } void setThemeMode(ThemeMode mode, {bool notify = true}) { themeMode = mode; if (notify) notifyListeners(); } void setDynamic(bool useDynamicColor, {bool notify = true}) { this.useDynamicColor = useDynamicColor; if (notify) notifyListeners(); } void setFontFamily(bool useSystemFont, {bool notify = true}) { currentFontFamily = useSystemFont ? null : customAppFontFamily; if (notify) notifyListeners(); } } ================================================ FILE: lib/bean/widget/collect_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:flutter_modular/flutter_modular.dart'; class CollectButton extends StatefulWidget { CollectButton({ super.key, required this.bangumiItem, this.color = Colors.white, this.onOpen, this.onClose, }) { isExtended = false; } CollectButton.extend({ super.key, required this.bangumiItem, this.color = Colors.white, this.onOpen, this.onClose, }) { isExtended = true; } final BangumiItem bangumiItem; final Color color; late final bool isExtended; final void Function()? onOpen; final void Function()? onClose; @override State createState() => _CollectButtonState(); } class _CollectButtonState extends State { // 1. 在看 // 2. 想看 // 3. 搁置 // 4. 看过 // 5. 抛弃 late int collectType; final CollectController collectController = Modular.get(); @override void initState() { super.initState(); } String getTypeStringByInt(int collectType) { switch (collectType) { case 1: return "在看"; case 2: return "想看"; case 3: return "搁置"; case 4: return "看过"; case 5: return "抛弃"; default: return "未追"; } } IconData getIconByInt(int collectType) { switch (collectType) { case 1: return Icons.favorite; case 2: return Icons.star_rounded; case 3: return Icons.pending_actions; case 4: return Icons.done; case 5: return Icons.heart_broken; default: return Icons.favorite_border; } } @override Widget build(BuildContext context) { collectType = collectController.getCollectType(widget.bangumiItem); return MenuAnchor( consumeOutsideTap: true, onClose: widget.onClose, onOpen: widget.onOpen, crossAxisUnconstrained: false, builder: (_, MenuController controller, __) { if (widget.isExtended) { return FilledButton.icon( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: Icon(getIconByInt(collectType)), label: Text(getTypeStringByInt(collectType)), ); } else { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: Icon( getIconByInt(collectType), color: widget.color, ), ); } }, menuChildren: List.generate( 6, (int index) => MenuItemButton( onPressed: () { if (index != collectType && mounted) { collectController.addCollect(widget.bangumiItem, type: index); setState(() {}); } }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( getIconByInt(index), color: index == collectType ? Theme.of(context).colorScheme.primary : null, ), SizedBox(width: 4), Text( ' ${getTypeStringByInt(index)}', style: TextStyle( color: index == collectType ? Theme.of(context).colorScheme.primary : null, ), ), ], ), ), ), ), ), ); } } ================================================ FILE: lib/bean/widget/custom_dropdown_menu.dart ================================================ import 'package:flutter/material.dart'; /// A custom dropdown menu widget that provides smooth animations without flickering. /// /// This widget was created to solve the visual flickering issue in Flutter's built-in /// [PopupMenuButton] where menu items are rendered before the animation completes, /// causing an uncoordinated visual effect. class CustomDropdownMenu extends StatelessWidget { final Offset offset; final Size buttonSize; final Animation animation; final List items; final String Function(String) itemBuilder; final double? maxHeight; /// Minimum width constraint for the menu. Defaults to 140. /// Note: If [maxWidth] is less than [minWidth], [minWidth] will be used as both min and max. final double? minWidth; /// Maximum width constraint for the menu. Defaults to 200. /// Note: If this value is less than [minWidth], it will be adjusted to equal [minWidth]. final double? maxWidth; final double gap; const CustomDropdownMenu({ super.key, required this.offset, required this.buttonSize, required this.animation, required this.items, required this.itemBuilder, this.maxHeight, this.minWidth, this.maxWidth, this.gap = 4, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); // Ensure width constraints are valid (minWidth <= maxWidth) final computedMinWidth = minWidth ?? 140; final computedMaxWidth = maxWidth ?? 200; final normalizedMinWidth = computedMinWidth; final normalizedMaxWidth = computedMaxWidth < computedMinWidth ? computedMinWidth : computedMaxWidth; return GestureDetector( onTap: () => Navigator.pop(context), behavior: HitTestBehavior.opaque, child: Stack( children: [ Positioned( left: offset.dx, top: offset.dy + buttonSize.height + gap, child: Material( elevation: 6, borderRadius: BorderRadius.circular(8), color: theme.colorScheme.surface, surfaceTintColor: Colors.transparent, shadowColor: Colors.black26, child: AnimatedBuilder( animation: animation, builder: (context, child) { final curvedValue = Curves.easeOutCubic.transform(animation.value); return ClipRect( child: Align( alignment: Alignment.topCenter, heightFactor: curvedValue, child: Opacity( opacity: curvedValue, child: child, ), ), ); }, child: Container( constraints: BoxConstraints( maxHeight: maxHeight ?? 350, minWidth: normalizedMinWidth, maxWidth: normalizedMaxWidth, ), child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, itemCount: items.length, itemBuilder: (context, index) { final itemValue = items[index]; final displayText = itemBuilder(itemValue); return InkWell( onTap: () => Navigator.pop(context, itemValue), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Text( displayText, style: const TextStyle(fontSize: 14), ), ), ); }, ), ), ), ), ), ], ), ); } } ================================================ FILE: lib/bean/widget/embedded_native_control_area.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:kazumi/utils/storage.dart'; class EmbeddedNativeControlArea extends StatefulWidget { /// The widget won't draw anything, just a placeholder for native window control. /// It only works on macOS at the moment. /// windows and linux have no way to embed native window control into flutter view. const EmbeddedNativeControlArea({ super.key, required this.child, this.requireOffset = true, }); final Widget child; final bool requireOffset; @override State createState() => _EmbeddedNativeControlAreaState(); } class _EmbeddedNativeControlAreaState extends State { bool showWindowButton = GStorage.setting.get(SettingBoxKey.showWindowButton, defaultValue: false); EdgeInsets get getInsets { if (!showWindowButton) { return EdgeInsets.zero; } if (!widget.requireOffset) { return EdgeInsets.zero; } if (Platform.isMacOS) { return const EdgeInsets.only(top: 22); } else { return EdgeInsets.zero; } } @override Widget build(BuildContext context) { return Padding( padding: getInsets, child: widget.child, ); } } ================================================ FILE: lib/bean/widget/error_widget.dart ================================================ import 'package:flutter/material.dart'; class GeneralErrorWidget extends StatelessWidget { const GeneralErrorWidget({ required this.errMsg, this.actions, super.key, }); final String errMsg; final List? actions; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return ConstrainedBox( constraints: BoxConstraints( maxWidth: constraints.maxWidth * 2 / 3, ), child: Text( errMsg, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleSmall, ), ); }, ), const SizedBox(height: 20), if (actions != null) Wrap( alignment: WrapAlignment.center, spacing: 8, runSpacing: 8, children: actions!, ), ], ); } } class GeneralErrorButton extends StatelessWidget { const GeneralErrorButton({ super.key, required this.onPressed, required this.text, }); final Function() onPressed; final String text; @override Widget build(BuildContext context) { return FilledButton.tonal( onPressed: onPressed, style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((_) { return Theme.of(context).colorScheme.primary.withAlpha(20); }), ), child: Text( text, style: TextStyle(color: Theme.of(context).colorScheme.primary), ), ); } } ================================================ FILE: lib/bean/widget/scrollable_wrapper.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; /// 滚动容器 /// 支持鼠标滚轮滚动和拖动滚动 /// 传入ListView的scrollController class ScrollableWrapper extends StatelessWidget { final Widget child; final ScrollController scrollController; const ScrollableWrapper({ super.key, required this.child, required this.scrollController, }); @override Widget build(BuildContext context) { return MouseRegion( child: Listener( onPointerSignal: (pointerSignal) { // 鼠标滚轮滚动 if (pointerSignal is PointerScrollEvent && scrollController.hasClients) { scrollController.position.moveTo( scrollController.offset + pointerSignal.scrollDelta.dy, curve: Curves.linear, ); } }, child: GestureDetector( onPanUpdate: (details) { // 拖动滚动 if (scrollController.hasClients) { scrollController.position.moveTo( scrollController.offset - details.delta.dx, curve: Curves.linear, ); } }, child: child, ), ), ); } } ================================================ FILE: lib/hive_registrar.g.dart ================================================ // Generated by Hive CE // Do not modify // Check in to version control import 'package:hive_ce/hive.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/bangumi/bangumi_tag.dart'; import 'package:kazumi/modules/collect/collect_change_module.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/modules/search/search_history_module.dart'; extension HiveRegistrar on HiveInterface { void registerAdapters() { registerAdapter(BangumiItemAdapter()); registerAdapter(BangumiTagAdapter()); registerAdapter(CollectedBangumiAdapter()); registerAdapter(CollectedBangumiChangeAdapter()); registerAdapter(DownloadEpisodeAdapter()); registerAdapter(DownloadRecordAdapter()); registerAdapter(HistoryAdapter()); registerAdapter(ProgressAdapter()); registerAdapter(SearchHistoryAdapter()); } } extension IsolatedHiveRegistrar on IsolatedHiveInterface { void registerAdapters() { registerAdapter(BangumiItemAdapter()); registerAdapter(BangumiTagAdapter()); registerAdapter(CollectedBangumiAdapter()); registerAdapter(CollectedBangumiChangeAdapter()); registerAdapter(DownloadEpisodeAdapter()); registerAdapter(DownloadRecordAdapter()); registerAdapter(HistoryAdapter()); registerAdapter(ProgressAdapter()); registerAdapter(SearchHistoryAdapter()); } } ================================================ FILE: lib/main.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:kazumi/app_module.dart'; import 'package:kazumi/app_widget.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/settings/theme_provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:kazumi/request/request.dart'; import 'package:kazumi/utils/proxy_manager.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:media_kit/media_kit.dart'; import 'package:window_manager/window_manager.dart'; import 'package:kazumi/pages/error/storage_error_page.dart'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent, statusBarColor: Colors.transparent, )); } if (Platform.isAndroid) { await Utils.checkWebViewFeatureSupport(); } try { final hivePath = '${(await getApplicationSupportDirectory()).path}/hive'; await Hive.initFlutter(hivePath); await GStorage.init(); } catch (e) { // Log the error for debugging (if logger is available) debugPrint('Storage initialization failed: $e'); if (Platform.isWindows) { await windowManager.ensureInitialized(); windowManager.waitUntilReadyToShow(null, () async { // Native window show has been blocked in `flutter_windows.cppL36` to avoid flickering. // Without this. the window will never show on Windows. await windowManager.show(); await windowManager.focus(); }); } runApp(MaterialApp( title: '初始化失败', localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [ Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN") ], locale: const Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"), builder: (context, child) { return const StorageErrorPage(); })); return; } bool showWindowButton = await GStorage.setting .get(SettingBoxKey.showWindowButton, defaultValue: false); if (Utils.isDesktop()) { await windowManager.ensureInitialized(); bool isLowResolution = await Utils.isLowResolution(); WindowOptions windowOptions = WindowOptions( size: isLowResolution ? const Size(840, 600) : const Size(1280, 860), center: true, skipTaskbar: false, // macOS always hide title bar regardless of showWindowButton setting titleBarStyle: (Platform.isMacOS || !showWindowButton) ? TitleBarStyle.hidden : TitleBarStyle.normal, windowButtonVisibility: showWindowButton, title: 'Kazumi', ); windowManager.waitUntilReadyToShow(windowOptions, () async { // Native window show has been blocked in `flutter_windows.cppL36` to avoid flickering. // Without this. the window will never show on Windows. await windowManager.show(); await windowManager.focus(); }); } Request(); await Request.setCookie(); ProxyManager.applyProxy(); runApp( ChangeNotifierProvider( create: (_) => ThemeProvider(), child: ModularApp( module: AppModule(), child: const AppWidget(), ), ), ); } ================================================ FILE: lib/modules/bangumi/bangumi_item.dart ================================================ import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/modules/bangumi/bangumi_tag.dart'; part 'bangumi_item.g.dart'; @HiveType(typeId: 0) class BangumiItem { @HiveField(0) int id; @HiveField(1) int type; @HiveField(2) String name; @HiveField(3) String nameCn; @HiveField(4) String summary; @HiveField(5) String airDate; @HiveField(6) int airWeekday; @HiveField(7) int rank; @HiveField(8) Map images; @HiveField(9, defaultValue: []) List tags; @HiveField(10, defaultValue: []) List alias; @HiveField(11, defaultValue: 0.0) double ratingScore; @HiveField(12, defaultValue: 0) int votes; @HiveField(13, defaultValue: []) List votesCount; @HiveField(14, defaultValue: '') String info; BangumiItem({ required this.id, required this.type, required this.name, required this.nameCn, required this.summary, required this.airDate, required this.airWeekday, required this.rank, required this.images, required this.tags, required this.alias, required this.ratingScore, required this.votes, required this.votesCount, required this.info, }); factory BangumiItem.fromJson(Map json) { List parseBangumiAliases(Map jsonData) { if (jsonData.containsKey('infobox') && jsonData['infobox'] is List) { final List infobox = jsonData['infobox']; for (var item in infobox) { if (item is Map && item['key'] == '别名') { final dynamic value = item['value']; if (value is List) { return value .map((element) { if (element is Map && element.containsKey('v')) { return element['v'].toString(); } return ''; }) .where((alias) => alias.isNotEmpty) .toList(); } } } } return []; } List parseBangumiVoteCount(Map jsonData) { if (!jsonData.containsKey('rating')) { return []; } final json = jsonData['rating']['count']; // For api.bgm.tv if (json is Map) { return List.generate(10, (i) => json['${i+1}'] as int); } // For next.bgm.tv if (json is List) { return json.map((e) => e as int).toList(); } return []; } List list = json['tags'] ?? []; List bangumiAlias = parseBangumiAliases(json); List tagList = list.map((i) => BangumiTag.fromJson(i)).toList(); List voteList = parseBangumiVoteCount(json); return BangumiItem( id: json['id'], type: json['type'] ?? 2, name: json['name'] ?? '', nameCn: (json['name_cn'] ?? '') == '' ? (((json['nameCN'] ?? '') == '') ? json['name'] : json['nameCN']) : json['name_cn'], summary: json['summary'] ?? '', airDate: json['date'] ?? '', airWeekday: Utils.dateStringToWeekday(json['date'] ?? '2000-11-11'), rank: json['rating']['rank'] ?? 0, images: Map.from( json['images'] ?? { "large": json['image'], "common": "", "medium": "", "small": "", "grid": "" }, ), tags: tagList, alias: bangumiAlias, ratingScore: double.parse( (json['rating']['score'] ?? 0.0).toDouble().toStringAsFixed(1)), votes: json['rating']['total'] ?? 0, votesCount: voteList, info: json['info'] ?? '', ); } } ================================================ FILE: lib/modules/bangumi/bangumi_item.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'bangumi_item.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class BangumiItemAdapter extends TypeAdapter { @override final typeId = 0; @override BangumiItem read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return BangumiItem( id: (fields[0] as num).toInt(), type: (fields[1] as num).toInt(), name: fields[2] as String, nameCn: fields[3] as String, summary: fields[4] as String, airDate: fields[5] as String, airWeekday: (fields[6] as num).toInt(), rank: (fields[7] as num).toInt(), images: (fields[8] as Map).cast(), tags: fields[9] == null ? [] : (fields[9] as List).cast(), alias: fields[10] == null ? [] : (fields[10] as List).cast(), ratingScore: fields[11] == null ? 0.0 : (fields[11] as num).toDouble(), votes: fields[12] == null ? 0 : (fields[12] as num).toInt(), votesCount: fields[13] == null ? [] : (fields[13] as List).cast(), info: fields[14] == null ? '' : fields[14] as String, ); } @override void write(BinaryWriter writer, BangumiItem obj) { writer ..writeByte(15) ..writeByte(0) ..write(obj.id) ..writeByte(1) ..write(obj.type) ..writeByte(2) ..write(obj.name) ..writeByte(3) ..write(obj.nameCn) ..writeByte(4) ..write(obj.summary) ..writeByte(5) ..write(obj.airDate) ..writeByte(6) ..write(obj.airWeekday) ..writeByte(7) ..write(obj.rank) ..writeByte(8) ..write(obj.images) ..writeByte(9) ..write(obj.tags) ..writeByte(10) ..write(obj.alias) ..writeByte(11) ..write(obj.ratingScore) ..writeByte(12) ..write(obj.votes) ..writeByte(13) ..write(obj.votesCount) ..writeByte(14) ..write(obj.info); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is BangumiItemAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/bangumi/bangumi_tag.dart ================================================ import 'package:hive_ce/hive.dart'; part 'bangumi_tag.g.dart'; @HiveType(typeId: 4) class BangumiTag { @HiveField(0) final String name; @HiveField(1) final int count; @HiveField(2) final int totalCount; BangumiTag({ required this.name, required this.count, required this.totalCount, }); factory BangumiTag.fromJson(Map json) { return BangumiTag( name: json['name'] ?? '', count: json['count'] ?? 0, totalCount: json['total_cont'] ?? 0, ); } Map toJson() { return { 'name': name, 'count': count, 'total_cont': totalCount, }; } } ================================================ FILE: lib/modules/bangumi/bangumi_tag.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'bangumi_tag.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class BangumiTagAdapter extends TypeAdapter { @override final typeId = 4; @override BangumiTag read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return BangumiTag( name: fields[0] as String, count: (fields[1] as num).toInt(), totalCount: (fields[2] as num).toInt(), ); } @override void write(BinaryWriter writer, BangumiTag obj) { writer ..writeByte(3) ..writeByte(0) ..write(obj.name) ..writeByte(1) ..write(obj.count) ..writeByte(2) ..write(obj.totalCount); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is BangumiTagAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/bangumi/episode_item.dart ================================================ class EpisodeInfo { int id; num episode; int type; String name; String nameCn; EpisodeInfo({ required this.id, required this.episode, required this.type, required this.name, required this.nameCn, }); factory EpisodeInfo.fromJson(Map json) { return EpisodeInfo( id: json['id'] ?? 0, episode: json['sort'] ?? 0, type: json['type'] ?? 0, name: json['name'] ?? '', nameCn: json['name_cn'] ?? ''); } factory EpisodeInfo.fromTemplate() { return EpisodeInfo(id: 0, episode: 0, type: 0, name: '', nameCn: ''); } void reset() { id = 0; episode = 0; type = 0; name = ''; nameCn = ''; } String readType() { switch (type) { case 0: return 'ep'; case 1: return 'sp'; case 2: return 'op'; case 3: return 'ed'; default: return ''; } } } ================================================ FILE: lib/modules/bangumi/weekday_item.dart ================================================ class Weekday { String? en; String? cn; String? ja; int? id; Weekday({this.en, this.cn, this.ja, this.id}); factory Weekday.fromJson(Map json) { return Weekday( en: json['en'], cn: json['cn'], ja: json['ja'], id: json['id'], ); } } ================================================ FILE: lib/modules/character/character_full_item.dart ================================================ class CharacterFullItem { final int id; final String name; final String nameCN; final String info; final String summary; final String image; CharacterFullItem({ required this.id, required this.name, required this.nameCN, required this.info, required this.summary, required this.image, }); factory CharacterFullItem.fromJson(Map json) { return CharacterFullItem( id: json['id'] ?? 0, name: json['name'] ?? '', nameCN: json['nameCN'] ?? '', info: json['info'] ?? '', summary: json['summary'] ?? '', image: json['images']['large'] ?? '', ); } factory CharacterFullItem.fromTemplate() { return CharacterFullItem( id: 0, name: '', nameCN: '', info: '', summary: '', image: '', ); } Map toJson() { return { 'id': id, 'name': name, 'nameCN': nameCN, 'info': info, 'summary': summary, }; } } ================================================ FILE: lib/modules/characters/actor_item.dart ================================================ class ActorAvator { final String small; final String medium; final String grid; final String large; ActorAvator({ required this.small, required this.medium, required this.grid, required this.large, }); factory ActorAvator.fromJson(Map json) { return ActorAvator( small: json['small'] ?? '', medium: json['medium'] ?? '', grid: json['grid'] ?? '', large: json['large'] ?? '', ); } Map toJson() { return { 'small': small, 'medium': medium, 'grid': grid, 'large': large, }; } } class ActorItem { final int id; final int type; final String name; final ActorAvator avator; ActorItem({ required this.id, required this.type, required this.name, required this.avator, }); factory ActorItem.fromJson(Map json) { return ActorItem( id: json['id'] ?? 0, type: json['type'] ?? 0, name: json['name'] ?? '', avator: ActorAvator.fromJson(json['images'] as Map), ); } Map toJson() { return { 'id': id, 'type': type, 'name': name, 'images': avator.toJson(), }; } } ================================================ FILE: lib/modules/characters/character_item.dart ================================================ import 'package:kazumi/modules/characters/actor_item.dart'; class CharacterAvator { final String small; final String medium; final String grid; final String large; CharacterAvator({ required this.small, required this.medium, required this.grid, required this.large, }); factory CharacterAvator.fromJson(Map json) { return CharacterAvator( small: json['small'] ?? '', medium: json['medium'] ?? '', grid: json['grid'] ?? '', large: json['large'] ?? '', ); } Map toJson() { return { 'small': small, 'medium': medium, 'grid': grid, 'large': large, }; } } class CharacterExtraInfo { String nameCn; String summary; CharacterExtraInfo({required this.nameCn, required this.summary}); factory CharacterExtraInfo.fromJson(Map json) { String nameCn = ''; final String hasNameCn = json['infobox'][0]['key']; if (hasNameCn == '简体中文名') { nameCn = json['infobox'][0]['value']; } return CharacterExtraInfo( nameCn: nameCn, summary: json['summary'] ); } } class CharacterItem { final int id; final int type; final String name; final String relation; final CharacterAvator avator; final List actorList; CharacterExtraInfo info; CharacterItem({ required this.id, required this.type, required this.name, required this.relation, required this.avator, required this.actorList, required this.info }); factory CharacterItem.fromJson(Map json) { var list = json['actors'] as List; List resActorList = list.map((i) => ActorItem.fromJson(i)).toList(); return CharacterItem( id: json['id'] ?? 0, type: json['type'] ?? 0, name: json['name'] ?? '', relation: json['relation'] ?? '未知', avator: CharacterAvator.fromJson(json['images'] as Map), actorList: resActorList, info: CharacterExtraInfo(nameCn: '', summary: '') ); } Map toJson() { return { 'id': id, 'type': type, 'name': name, 'relation': relation, 'images': avator.toJson(), 'actors': actorList.map((e) => e.toJson()).toList(), }; } } ================================================ FILE: lib/modules/characters/characters_response.dart ================================================ import 'package:kazumi/request/api.dart'; import 'package:kazumi/modules/characters/character_item.dart'; /// The response from [Api.bangumiInfoByID] /// It contains a list of [CharacterItem] /// It is used to show general information about seraval bangumi characters class CharactersResponse { final List charactersList; CharactersResponse({ required this.charactersList, }); factory CharactersResponse.fromJson(List list) { List resCharactersList = list.map((i) => CharacterItem.fromJson(i)).toList(); return CharactersResponse( charactersList: resCharactersList, ); } factory CharactersResponse.fromTemplate() { return CharactersResponse( charactersList: [], ); } Map toJson() { return { 'charactersList': charactersList.map((e) => e.toJson()).toList(), }; } } ================================================ FILE: lib/modules/collect/collect_change_module.dart ================================================ import 'package:hive_ce/hive.dart'; part 'collect_change_module.g.dart'; // The box stores the changes history of collected bangumi // The changes will be used to sync with webDav @HiveType(typeId: 5) class CollectedBangumiChange { // timestamp in seconds // hivebox has limited the length of key, the max number is 4294967295 // we have to use timestamp in seconds as key to avoid key conflict and hive key limit @HiveField(0) int id; @HiveField(1) int bangumiID; // 1. add // 2. update // 3. delete @HiveField(2) int action; // 1. 在看 // 2. 想看 // 3. 搁置 // 4. 看过 // 5. 抛弃 @HiveField(3) int type; @HiveField(4) int timestamp; CollectedBangumiChange(this.id, this.bangumiID, this.action,this.type, this.timestamp); @override String toString() { return 'id: $id, bangumi: $bangumiID, action: $action, time: $timestamp'; } } ================================================ FILE: lib/modules/collect/collect_change_module.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'collect_change_module.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class CollectedBangumiChangeAdapter extends TypeAdapter { @override final typeId = 5; @override CollectedBangumiChange read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return CollectedBangumiChange( (fields[0] as num).toInt(), (fields[1] as num).toInt(), (fields[2] as num).toInt(), (fields[3] as num).toInt(), (fields[4] as num).toInt(), ); } @override void write(BinaryWriter writer, CollectedBangumiChange obj) { writer ..writeByte(5) ..writeByte(0) ..write(obj.id) ..writeByte(1) ..write(obj.bangumiID) ..writeByte(2) ..write(obj.action) ..writeByte(3) ..write(obj.type) ..writeByte(4) ..write(obj.timestamp); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is CollectedBangumiChangeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/collect/collect_module.dart ================================================ import 'package:hive_ce/hive.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; part 'collect_module.g.dart'; @HiveType(typeId: 3) class CollectedBangumi { @HiveField(0) BangumiItem bangumiItem; @HiveField(1) DateTime time; // 1. 在看 // 2. 想看 // 3. 搁置 // 4. 看过 // 5. 抛弃 @HiveField(2) int type; String get key => bangumiItem.id.toString(); CollectedBangumi(this.bangumiItem, this.time, this.type); static String getKey(BangumiItem bangumiItem) => bangumiItem.id.toString(); @override String toString() { return 'type: $type, time: $time, anime: ${bangumiItem.name}'; } } ================================================ FILE: lib/modules/collect/collect_module.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'collect_module.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class CollectedBangumiAdapter extends TypeAdapter { @override final typeId = 3; @override CollectedBangumi read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return CollectedBangumi( fields[0] as BangumiItem, fields[1] as DateTime, (fields[2] as num).toInt(), ); } @override void write(BinaryWriter writer, CollectedBangumi obj) { writer ..writeByte(3) ..writeByte(0) ..write(obj.bangumiItem) ..writeByte(1) ..write(obj.time) ..writeByte(2) ..write(obj.type); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is CollectedBangumiAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/collect/collect_type.dart ================================================ /// 收藏类型枚举 /// /// 用于标识番剧的收藏状态 enum CollectType { /// 未收藏 none(0, '未收藏'), /// 在看 watching(1, '在看'), /// 想看 planToWatch(2, '想看'), /// 搁置 onHold(3, '搁置'), /// 看过 watched(4, '看过'), /// 抛弃 abandoned(5, '抛弃'); const CollectType(this.value, this.label); /// 数值表示 final int value; /// 显示标签 final String label; /// 根据数值获取枚举 static CollectType fromValue(int value) { return CollectType.values.firstWhere( (type) => type.value == value, orElse: () => CollectType.none, ); } /// 是否为有效的收藏状态(排除未收藏) bool get isCollected => this != CollectType.none; } ================================================ FILE: lib/modules/comments/comment_item.dart ================================================ class UserAvatar { final String small; final String medium; final String large; UserAvatar({ required this.small, required this.medium, required this.large, }); factory UserAvatar.fromJson(Map json) { return UserAvatar( small: json['small'] ?? '', medium: json['medium'] ?? '', large: json['large'] ?? '', ); } Map toJson() { return { 'small': small, 'medium': medium, 'large': large, }; } } class User { final int id; final String username; final String nickname; final UserAvatar avatar; final String sign; final int joinedAt; User({ required this.id, required this.username, required this.nickname, required this.avatar, required this.sign, required this.joinedAt, }); factory User.fromJson(Map json) { return User( id: json['id'] ?? 0, username: json['username'] ?? '', nickname: json['nickname'] ?? '', avatar: UserAvatar.fromJson(json['avatar'] as Map), sign: json['sign'] ?? '', joinedAt: json['joinedAt'] ?? 0, ); } Map toJson() { return { 'id': id, 'username': username, 'nickname': nickname, 'avatar': avatar.toJson(), 'sign': sign, 'joinedAt': joinedAt, }; } } class Comment { final int rate; final String comment; final int updatedAt; Comment({ required this.rate, required this.comment, required this.updatedAt, }); factory Comment.fromJson(Map json) { return Comment( rate: json['rate'] ?? 0, comment: json['comment'] ?? '', updatedAt: json['updatedAt'] ?? 0, ); } Map toJson() { return { 'rate': rate, 'comment': comment, 'updatedAt': updatedAt, }; } } class CommentItem { final User user; final Comment comment; CommentItem({ required this.user, required this.comment, }); factory CommentItem.fromJson(Map json) { return CommentItem( user: User.fromJson(json['user']), comment: Comment.fromJson(json), ); } Map toJson() { return { 'user': user.toJson(), 'comment': comment.toJson(), }; } } class EpisodeComment { final User user; final String comment; final int createdAt; EpisodeComment({ required this.user, required this.comment, required this.createdAt, }); factory EpisodeComment.fromJson(Map json) { return EpisodeComment( user: User.fromJson(json['user']), comment: json['content'] ?? '', createdAt: json['createdAt'] ?? 0, ); } Map toJson() { return { 'user': user.toJson(), 'content': comment, 'createdAt': createdAt, }; } } class EpisodeCommentItem { final EpisodeComment comment; final List replies; EpisodeCommentItem({ required this.comment, required this.replies }); factory EpisodeCommentItem.fromJson(Map json) { var list = json['replies'] as List; List tempList = list.map((i) => EpisodeComment.fromJson(i)).toList(); return EpisodeCommentItem( comment: EpisodeComment.fromJson(json), replies: tempList ); } Map toJson() { return { 'comment': comment.toJson(), 'list': replies, }; } } class CharacterComment { final User user; final String comment; final int createdAt; CharacterComment({ required this.user, required this.comment, required this.createdAt, }); factory CharacterComment.fromJson(Map json) { return CharacterComment( user: User.fromJson(json['user']), comment: json['content'] ?? '', createdAt: json['createdAt'] ?? 0, ); } Map toJson() { return { 'user': user.toJson(), 'content': comment, 'createdAt': createdAt, }; } } class CharacterCommentItem { final CharacterComment comment; final List replies; CharacterCommentItem({ required this.comment, required this.replies }); factory CharacterCommentItem.fromJson(Map json) { var list = json['replies'] as List; List tempList = list.map((i) => CharacterComment.fromJson(i)).toList(); return CharacterCommentItem( comment: CharacterComment.fromJson(json), replies: tempList ); } Map toJson() { return { 'comment': comment.toJson(), 'list': replies, }; } } ================================================ FILE: lib/modules/comments/comment_response.dart ================================================ import 'package:kazumi/modules/comments/comment_item.dart'; class CommentResponse { List commentList; int total; CommentResponse({ required this.commentList, required this.total, }); factory CommentResponse.fromJson(Map json) { List? list = (json['list'] as List?) ?? (json['data'] as List?); List? resCommentList = list?.map((i) => CommentItem.fromJson(i)).toList(); return CommentResponse( commentList: resCommentList ?? [], total: json['total'], ); } factory CommentResponse.fromTemplate() { return CommentResponse( commentList: [], total: 0, ); } Map toJson() { return { 'list': commentList, 'total': total, }; } } class EpisodeCommentResponse { List commentList; EpisodeCommentResponse({ required this.commentList, }); factory EpisodeCommentResponse.fromJson(List json) { List? resCommentList = (json as List?)?.map((i) => EpisodeCommentItem.fromJson(i)).toList(); return EpisodeCommentResponse( commentList: resCommentList ?? [], ); } factory EpisodeCommentResponse.fromTemplate() { return EpisodeCommentResponse( commentList: [], ); } Map toJson() { return { 'list': commentList, }; } } class CharacterCommentResponse { List commentList; CharacterCommentResponse({ required this.commentList, }); factory CharacterCommentResponse.fromJson(List json) { List? resCommentList = (json as List?)?.map((i) => CharacterCommentItem.fromJson(i)).toList(); return CharacterCommentResponse( commentList: resCommentList ?? [], ); } factory CharacterCommentResponse.fromTemplate() { return CharacterCommentResponse( commentList: [], ); } Map toJson() { return { 'list': commentList, }; } } ================================================ FILE: lib/modules/danmaku/danmaku_episode_response.dart ================================================ class DanmakuEpisode { int episodeId; String episodeTitle; DanmakuEpisode({ required this.episodeId, required this.episodeTitle, }); factory DanmakuEpisode.fromJson(Map json) { return DanmakuEpisode( episodeId: json['episodeId'], episodeTitle: json['episodeTitle'], ); } Map toJson() { return { 'episodeId': episodeId, 'episodeTitle': episodeTitle, }; } } class DanmakuEpisodeResponse { int bangumiId; List episodes; int errorCode; bool success; String errorMessage; DanmakuEpisodeResponse({ required this.bangumiId, required this.episodes, required this.errorCode, required this.success, required this.errorMessage, }); factory DanmakuEpisodeResponse.fromJson(Map json) { var list = json['bangumi']['episodes'] as List; List episodeList = list.map((i) => DanmakuEpisode.fromJson(i)).toList(); return DanmakuEpisodeResponse( bangumiId: json['bangumi']['animeId'], episodes: episodeList, errorCode: json['errorCode'], success: json['success'], errorMessage: json['errorMessage'], ); } factory DanmakuEpisodeResponse.fromTemplate() { return DanmakuEpisodeResponse( bangumiId: 0, episodes: [], errorCode: 0, success: false, errorMessage: '', ); } Map toJson() { return { 'bangumi': episodes.map((episode) => episode.toJson()).toList(), 'errorCode': errorCode, 'success': success, 'errorMessage': errorMessage, }; } } ================================================ FILE: lib/modules/danmaku/danmaku_module.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/utils/utils.dart'; class Danmaku { // 弹幕内容 String message; // 弹幕时间 double time; // 弹幕类型 (1-普通弹幕,4-底部弹幕,5-顶部弹幕) int type; // 弹幕颜色 Color color; // 弹幕来源 ([BiliBili], [Gamer]) String source; Danmaku({required this.message, required this.time, required this.type, required this.color, required this.source}); factory Danmaku.fromJson(Map json) { String messageValue = json['m']; List parts = json['p'].split(','); double timeValue = double.parse(parts[0]); int typeValue = int.parse(parts[1]); Color color = Utils.generateDanmakuColor(int.parse(parts[2])); String sourceValue = parts[3]; return Danmaku(time: timeValue, message: messageValue, type: typeValue, color: color, source: sourceValue); } /// 序列化为 JSON 格式 (与 fromJson 格式一致) Map toJson() { // 只存储 RGB 部分 (与 DanDanPlay API 格式一致) final colorValue = ((color.r * 255).toInt() << 16) | ((color.g * 255).toInt() << 8) | (color.b * 255).toInt(); return { 'm': message, 'p': '$time,$type,$colorValue,$source', }; } } ================================================ FILE: lib/modules/danmaku/danmaku_search_response.dart ================================================ class DanmakuAnime { int animeId; String animeTitle; String type; String typeDescription; String imageUrl; DateTime startDate; int episodeCount; double rating; bool isFavorited; DanmakuAnime({ required this.animeId, required this.animeTitle, required this.type, required this.typeDescription, required this.imageUrl, required this.startDate, required this.episodeCount, required this.rating, required this.isFavorited, }); factory DanmakuAnime.fromJson(Map json) { return DanmakuAnime( animeId: json['animeId'], animeTitle: json['animeTitle'], type: json['type'], typeDescription: json['typeDescription'], imageUrl: json['imageUrl'], startDate: DateTime.parse(json['startDate']), episodeCount: json['episodeCount'], rating: json['rating'].toDouble(), isFavorited: json['isFavorited'], ); } Map toJson() { return { 'animeId': animeId, 'animeTitle': animeTitle, 'type': type, 'typeDescription': typeDescription, 'imageUrl': imageUrl, 'startDate': startDate.toIso8601String(), 'episodeCount': episodeCount, 'rating': rating, 'isFavorited': isFavorited, }; } } class DanmakuSearchResponse { List animes; int errorCode; bool success; String errorMessage; DanmakuSearchResponse({ required this.animes, required this.errorCode, required this.success, required this.errorMessage, }); factory DanmakuSearchResponse.fromJson(Map json) { var list = json['animes'] as List; List animeList = list.map((i) => DanmakuAnime.fromJson(i)).toList(); return DanmakuSearchResponse( animes: animeList, errorCode: json['errorCode'], success: json['success'], errorMessage: json['errorMessage'], ); } Map toJson() { return { 'animes': animes.map((anime) => anime.toJson()).toList(), 'errorCode': errorCode, 'success': success, 'errorMessage': errorMessage, }; } } ================================================ FILE: lib/modules/download/download_module.dart ================================================ import 'package:hive_ce/hive.dart'; part 'download_module.g.dart'; @HiveType(typeId: 7) class DownloadRecord { @HiveField(0) int bangumiId; @HiveField(1) String bangumiName; @HiveField(2) String bangumiCover; @HiveField(3) String pluginName; @HiveField(4) Map episodes; @HiveField(5) DateTime createdAt; String get key => '${pluginName}_$bangumiId'; DownloadRecord( this.bangumiId, this.bangumiName, this.bangumiCover, this.pluginName, this.episodes, this.createdAt, ); } @HiveType(typeId: 8) class DownloadEpisode { @HiveField(0) int episodeNumber; @HiveField(1) String episodeName; @HiveField(2) int road; /// 0=pending 1=resolving 2=downloading 3=completed 4=failed 5=paused @HiveField(3) int status; @HiveField(4) double progressPercent; @HiveField(5) int totalSegments; @HiveField(6) int downloadedSegments; @HiveField(7) String localM3u8Path; @HiveField(8) String downloadDirectory; @HiveField(9) String networkM3u8Url; @HiveField(10) DateTime? completedAt; @HiveField(11, defaultValue: '') String errorMessage; @HiveField(12, defaultValue: 0) int totalBytes; @HiveField(13, defaultValue: '') String episodePageUrl; /// 缓存的弹幕数据 (JSON 字符串格式) @HiveField(14, defaultValue: '') String danmakuData; /// DanDanPlay 番剧 ID (用于弹幕查询缓存) @HiveField(15, defaultValue: 0) int danDanBangumiID; DownloadEpisode( this.episodeNumber, this.episodeName, this.road, this.status, this.progressPercent, this.totalSegments, this.downloadedSegments, this.localM3u8Path, this.downloadDirectory, this.networkM3u8Url, this.completedAt, this.errorMessage, this.totalBytes, this.episodePageUrl, { this.danmakuData = '', this.danDanBangumiID = 0, }); } class DownloadStatus { static const int pending = 0; static const int resolving = 1; static const int downloading = 2; static const int completed = 3; static const int failed = 4; static const int paused = 5; } ================================================ FILE: lib/modules/download/download_module.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'download_module.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class DownloadRecordAdapter extends TypeAdapter { @override final typeId = 7; @override DownloadRecord read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return DownloadRecord( (fields[0] as num).toInt(), fields[1] as String, fields[2] as String, fields[3] as String, (fields[4] as Map).cast(), fields[5] as DateTime, ); } @override void write(BinaryWriter writer, DownloadRecord obj) { writer ..writeByte(6) ..writeByte(0) ..write(obj.bangumiId) ..writeByte(1) ..write(obj.bangumiName) ..writeByte(2) ..write(obj.bangumiCover) ..writeByte(3) ..write(obj.pluginName) ..writeByte(4) ..write(obj.episodes) ..writeByte(5) ..write(obj.createdAt); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is DownloadRecordAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class DownloadEpisodeAdapter extends TypeAdapter { @override final typeId = 8; @override DownloadEpisode read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return DownloadEpisode( (fields[0] as num).toInt(), fields[1] as String, (fields[2] as num).toInt(), (fields[3] as num).toInt(), (fields[4] as num).toDouble(), (fields[5] as num).toInt(), (fields[6] as num).toInt(), fields[7] as String, fields[8] as String, fields[9] as String, fields[10] as DateTime?, fields[11] == null ? '' : fields[11] as String, fields[12] == null ? 0 : (fields[12] as num).toInt(), fields[13] == null ? '' : fields[13] as String, danmakuData: fields[14] == null ? '' : fields[14] as String, danDanBangumiID: fields[15] == null ? 0 : (fields[15] as num).toInt(), ); } @override void write(BinaryWriter writer, DownloadEpisode obj) { writer ..writeByte(16) ..writeByte(0) ..write(obj.episodeNumber) ..writeByte(1) ..write(obj.episodeName) ..writeByte(2) ..write(obj.road) ..writeByte(3) ..write(obj.status) ..writeByte(4) ..write(obj.progressPercent) ..writeByte(5) ..write(obj.totalSegments) ..writeByte(6) ..write(obj.downloadedSegments) ..writeByte(7) ..write(obj.localM3u8Path) ..writeByte(8) ..write(obj.downloadDirectory) ..writeByte(9) ..write(obj.networkM3u8Url) ..writeByte(10) ..write(obj.completedAt) ..writeByte(11) ..write(obj.errorMessage) ..writeByte(12) ..write(obj.totalBytes) ..writeByte(13) ..write(obj.episodePageUrl) ..writeByte(14) ..write(obj.danmakuData) ..writeByte(15) ..write(obj.danDanBangumiID); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is DownloadEpisodeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/history/history_module.dart ================================================ import 'package:hive_ce/hive.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; part 'history_module.g.dart'; @HiveType(typeId: 1) class History { @HiveField(0) Map progresses = {}; @HiveField(1) int lastWatchEpisode; @HiveField(2) String adapterName; @HiveField(3) BangumiItem bangumiItem; @HiveField(4) DateTime lastWatchTime; @HiveField(5) String lastSrc; @HiveField(6, defaultValue: '') String lastWatchEpisodeName; String get key => adapterName + bangumiItem.id.toString(); History( this.bangumiItem, this.lastWatchEpisode, this.adapterName, this.lastWatchTime, this.lastSrc, this.lastWatchEpisodeName); static String getKey(String n, BangumiItem s) => n + s.id.toString(); @override String toString() { return 'Adapter: $adapterName, anime: ${bangumiItem.name}'; } } @HiveType(typeId: 2) class Progress { @HiveField(0) int episode; @HiveField(1) int road; @HiveField(2) int _progressInMilli; Duration get progress => Duration(milliseconds: _progressInMilli); set progress(Duration d) => _progressInMilli = d.inMilliseconds; Progress(this.episode, this.road, this._progressInMilli); @override String toString() { return 'Episode ${episode.toString()}, progress $progress'; } } ================================================ FILE: lib/modules/history/history_module.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'history_module.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class HistoryAdapter extends TypeAdapter { @override final typeId = 1; @override History read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return History( fields[3] as BangumiItem, (fields[1] as num).toInt(), fields[2] as String, fields[4] as DateTime, fields[5] as String, fields[6] == null ? '' : fields[6] as String, )..progresses = (fields[0] as Map).cast(); } @override void write(BinaryWriter writer, History obj) { writer ..writeByte(7) ..writeByte(0) ..write(obj.progresses) ..writeByte(1) ..write(obj.lastWatchEpisode) ..writeByte(2) ..write(obj.adapterName) ..writeByte(3) ..write(obj.bangumiItem) ..writeByte(4) ..write(obj.lastWatchTime) ..writeByte(5) ..write(obj.lastSrc) ..writeByte(6) ..write(obj.lastWatchEpisodeName); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is HistoryAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class ProgressAdapter extends TypeAdapter { @override final typeId = 2; @override Progress read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return Progress( (fields[0] as num).toInt(), (fields[1] as num).toInt(), (fields[2] as num).toInt(), ); } @override void write(BinaryWriter writer, Progress obj) { writer ..writeByte(3) ..writeByte(0) ..write(obj.episode) ..writeByte(1) ..write(obj.road) ..writeByte(2) ..write(obj._progressInMilli); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is ProgressAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/plugin/plugin_http_module.dart ================================================ class PluginHTTPItem { String name; String version; bool useNativePlayer; String author; int lastUpdate; bool antiCrawlerEnabled; PluginHTTPItem({ required this.name, required this.version, required this.useNativePlayer, required this.author, required this.lastUpdate, this.antiCrawlerEnabled = false, }); factory PluginHTTPItem.fromJson(Map json) { final dynamic rawConfig = json['antiCrawlerConfig']; final bool antiCrawlerEnabled = rawConfig is Map ? (rawConfig['enabled'] as bool? ?? false) : (json['antiCrawlerEnabled'] as bool? ?? false); return PluginHTTPItem( name: json['name'], version: json['version'], useNativePlayer: json['useNativePlayer'], author: json['author'], lastUpdate: json['lastUpdate'] ?? 0, antiCrawlerEnabled: antiCrawlerEnabled, ); } } ================================================ FILE: lib/modules/roads/road_module.dart ================================================ class Road { String name; List data; List identifier; Road({ required this.name, required this.data, required this.identifier, }); } ================================================ FILE: lib/modules/search/plugin_search_module.dart ================================================ class SearchItem { String name; String src; SearchItem({ required this.name, required this.src, }); factory SearchItem.fromJson(Map json) { return SearchItem(name: json['name'], src: json['src']); } } class PluginSearchResponse { String pluginName; List data; PluginSearchResponse({ required this.pluginName, required this.data, }); factory PluginSearchResponse.fromJson(Map json) { return PluginSearchResponse( pluginName: json['pluginName'], data: (json['data'] as List) .map((itemJson) => SearchItem.fromJson(itemJson)) .toList(), ); } } ================================================ FILE: lib/modules/search/search_history_module.dart ================================================ import 'package:hive_ce/hive.dart'; part 'search_history_module.g.dart'; @HiveType(typeId: 6) class SearchHistory { @HiveField(0) String keyword; @HiveField(1) int timestamp; SearchHistory(this.keyword, this.timestamp); String get key => timestamp.toString(); @override String toString() { return 'Search keyword: $keyword, search time: ${DateTime.fromMillisecondsSinceEpoch(timestamp)}'; } } ================================================ FILE: lib/modules/search/search_history_module.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'search_history_module.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class SearchHistoryAdapter extends TypeAdapter { @override final typeId = 6; @override SearchHistory read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return SearchHistory( fields[0] as String, (fields[1] as num).toInt(), ); } @override void write(BinaryWriter writer, SearchHistory obj) { writer ..writeByte(2) ..writeByte(0) ..write(obj.keyword) ..writeByte(1) ..write(obj.timestamp); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is SearchHistoryAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } ================================================ FILE: lib/modules/staff/staff_item.dart ================================================ class StaffFullItem { final Staff staff; final List positions; StaffFullItem({ required this.staff, required this.positions, }); factory StaffFullItem.fromJson(Map json) { return StaffFullItem( staff: json['staff'] != null ? Staff.fromJson(json['staff'] as Map) : Staff.fromTemplate(), positions: (json['positions'] as List? ?? []) .map((item) => Position.fromJson(item as Map)) .toList(), ); } Map toJson() { return { 'staff': staff.toJson(), 'positions': positions.map((item) => item.toJson()).toList(), }; } } class Staff { final int id; final String name; final String nameCN; final int type; final String info; final int comment; final bool lock; final bool nsfw; final Images? images; Staff({ required this.id, required this.name, required this.nameCN, required this.type, required this.info, required this.comment, required this.lock, required this.nsfw, this.images, }); factory Staff.fromJson(Map json) { return Staff( id: json['id'] is int ? json['id'] as int : 0, name: json['name'] as String? ?? '', nameCN: json['nameCN'] as String? ?? '', type: json['type'] is int ? json['type'] as int : 0, info: json['info'] as String? ?? '', comment: json['comment'] is int ? json['comment'] as int : 0, lock: json['lock'] as bool? ?? false, nsfw: json['nsfw'] as bool? ?? false, images: json['images'] != null ? Images.fromJson(json['images'] as Map) : null, ); } factory Staff.fromTemplate() { return Staff( id: 0, name: '', nameCN: '', type: 0, info: '', comment: 0, lock: false, nsfw: false, images: null, ); } Map toJson() { final data = { 'id': id, 'name': name, 'nameCN': nameCN, 'type': type, 'info': info, 'comment': comment, 'lock': lock, 'nsfw': nsfw, }; if (images != null) { data['images'] = images!.toJson(); } return data; } } class Images { final String large; final String medium; final String small; final String grid; Images({ required this.large, required this.medium, required this.small, required this.grid, }); factory Images.fromJson(Map json) { return Images( large: json['large'] as String? ?? '', medium: json['medium'] as String? ?? '', small: json['small'] as String? ?? '', grid: json['grid'] as String? ?? '', ); } Map toJson() { return { 'large': large, 'medium': medium, 'small': small, 'grid': grid, }; } } class Position { final PositionType type; final String summary; final String appearEps; Position({ required this.type, required this.summary, required this.appearEps, }); factory Position.fromJson(Map json) { return Position( type: json['type'] != null ? PositionType.fromJson(json['type'] as Map) : PositionType.fromTemplate(), summary: json['summary'] as String? ?? '', appearEps: json['appearEps'] as String? ?? '', ); } Map toJson() { return { 'type': type.toJson(), 'summary': summary, 'appearEps': appearEps, }; } } class PositionType { final int id; final String en; final String cn; final String jp; PositionType({ required this.id, required this.en, required this.cn, required this.jp, }); factory PositionType.fromJson(Map json) { return PositionType( id: json['id'] is int ? json['id'] as int : 0, en: json['en'] as String? ?? '', cn: json['cn'] as String? ?? '', jp: json['jp'] as String? ?? '', ); } factory PositionType.fromTemplate() { return PositionType( id: 0, en: '', cn: '', jp: '', ); } Map toJson() { return { 'id': id, 'en': en, 'cn': cn, 'jp': jp, }; } } ================================================ FILE: lib/modules/staff/staff_response.dart ================================================ import 'package:kazumi/modules/staff/staff_item.dart'; class StaffResponse { final List data; final int total; StaffResponse({ required this.data, required this.total, }); factory StaffResponse.fromJson(Map json) { return StaffResponse( data: (json['data'] as List? ?? []) .map((item) => StaffFullItem.fromJson(item as Map)) .toList(), total: json['total'] is int ? json['total'] as int : 0, ); } factory StaffResponse.fromTemplate() { return StaffResponse( data: [], total: 0, ); } Map toJson() { return { 'data': data.map((item) => item.toJson()).toList(), 'total': total, }; } } ================================================ FILE: lib/pages/about/about_module.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/pages/about/about_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/logs/logs_page.dart'; class AboutModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/", child: (_) => const AboutPage()); r.child("/logs", child: (_) => const LogsPage()); r.child( "/license", child: (_) => const LicensePage( applicationName: 'Kazumi', applicationVersion: Api.version, applicationLegalese: '开源许可证', ), ); } } ================================================ FILE: lib/pages/about/about_page.dart ================================================ import 'dart:io'; import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/my/my_controller.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/mortis.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutPage extends StatefulWidget { const AboutPage({super.key}); @override State createState() => _AboutPageState(); } class _AboutPageState extends State { final exitBehaviorTitles = ['退出 Kazumi', '最小化至托盘', '每次都询问']; late dynamic defaultDanmakuArea; late dynamic defaultThemeMode; late dynamic defaultThemeColor; Box setting = GStorage.setting; late int exitBehavior = setting.get(SettingBoxKey.exitBehavior, defaultValue: 2); late bool autoUpdate; double _cacheSizeMB = -1; final MyController myController = Modular.get(); final MenuController menuController = MenuController(); @override void initState() { super.initState(); autoUpdate = setting.get(SettingBoxKey.autoUpdate, defaultValue: true); _getCacheSize(); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } Future _getCacheDir() async { Directory tempDir = await getTemporaryDirectory(); return Directory('${tempDir.path}/libCachedImageData'); } Future _getCacheSize() async { Directory cacheDir = await _getCacheDir(); if (await cacheDir.exists()) { int totalSizeBytes = await _getTotalSizeOfFilesInDir(cacheDir); double totalSizeMB = (totalSizeBytes / (1024 * 1024)); if (mounted) { setState(() { _cacheSizeMB = totalSizeMB; }); } } else { if (mounted) { setState(() { _cacheSizeMB = 0.0; }); } } } Future _getTotalSizeOfFilesInDir(final Directory directory) async { final List children = directory.listSync(); int total = 0; try { for (final FileSystemEntity child in children) { if (child is File) { final int length = await child.length(); total += length; } else if (child is Directory) { total += await _getTotalSizeOfFilesInDir(child); } } } catch (_) {} return total; } Future _clearCache() async { final Directory libCacheDir = await _getCacheDir(); await libCacheDir.delete(recursive: true); _getCacheSize(); } void _showCacheDialog() { KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('缓存管理'), content: const Text('缓存为番剧封面, 清除后加载时需要重新下载,确认要清除缓存吗?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { try { _clearCache(); } catch (_) {} KazumiDialog.dismiss(); }, child: const Text('确认'), ), ], ); }, ); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) async { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('关于')), // backgroundColor: Colors.transparent, body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/about/license'); }, title: Text('开源许可证', style: TextStyle(fontFamily: fontFamily)), description: Text('查看所有开源许可证', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('外部链接', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(Api.projectUrl), mode: LaunchMode.externalApplication); }, title: Text('项目主页', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(Api.sourceUrl), mode: LaunchMode.externalApplication); }, title: Text('代码仓库', style: TextStyle(fontFamily: fontFamily)), value: Text('Github', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(Api.iconUrl), mode: LaunchMode.externalApplication); }, title: Text('图标创作', style: TextStyle(fontFamily: fontFamily)), value: Text('Pixiv', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(Api.bangumiIndex), mode: LaunchMode.externalApplication); }, title: Text('番剧索引', style: TextStyle(fontFamily: fontFamily)), value: Text('Bangumi', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(Api.dandanIndex), mode: LaunchMode.externalApplication); }, title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)), description: Text('ID: ${mortis['id']}', style: TextStyle(fontFamily: fontFamily)), value: Text('DanDanPlay', style: TextStyle(fontFamily: fontFamily)), ), ], ), if (Utils.isDesktop()) // 之后如果有非桌面平台的新选项可以移除 SettingsSection( title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { if (menuController.isOpen) { menuController.close(); } else { menuController.open(); } }, title: Text('关闭时', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: menuController, builder: (_, __, ___) { return Text(exitBehaviorTitles[exitBehavior]); }, menuChildren: [ for (int i = 0; i < 3; i++) MenuItemButton( requestFocusOnHover: false, onPressed: () { exitBehavior = i; setting.put(SettingBoxKey.exitBehavior, i); setState(() {}); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( exitBehaviorTitles[i], style: TextStyle( color: i == exitBehavior ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ], ), ), ], ), SettingsSection( tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/about/logs'); }, title: Text('错误日志', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( tiles: [ SettingsTile.navigation( onPressed: (_) { _showCacheDialog(); }, title: Text('清除缓存', style: TextStyle(fontFamily: fontFamily)), value: _cacheSizeMB == -1 ? Text('统计中...', style: TextStyle(fontFamily: fontFamily)) : Text('${_cacheSizeMB.toStringAsFixed(2)}MB', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('应用更新', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { autoUpdate = value ?? !autoUpdate; await setting.put(SettingBoxKey.autoUpdate, autoUpdate); setState(() {}); }, title: Text('自动更新', style: TextStyle(fontFamily: fontFamily)), initialValue: autoUpdate, ), SettingsTile.navigation( onPressed: (_) { myController.checkUpdate(); }, title: Text('检查更新', style: TextStyle(fontFamily: fontFamily)), value: Text('当前版本 ${Api.version}', style: TextStyle(fontFamily: fontFamily)), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/collect/collect_controller.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:kazumi/modules/collect/collect_change_module.dart'; import 'package:kazumi/modules/collect/collect_type.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/webdav.dart'; import 'package:kazumi/repositories/collect_crud_repository.dart'; import 'package:kazumi/repositories/collect_repository.dart'; import 'package:hive_ce/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:kazumi/utils/logger.dart'; part 'collect_controller.g.dart'; class CollectController = _CollectController with _$CollectController; abstract class _CollectController with Store { final _collectCrudRepository = Modular.get(); final _collectRepository = Modular.get(); Box setting = GStorage.setting; List get favorites => _collectCrudRepository.getFavorites(); @observable ObservableList collectibles = ObservableList(); void loadCollectibles() { collectibles.clear(); collectibles.addAll(_collectCrudRepository.getAllCollectibles()); } int getCollectType(BangumiItem bangumiItem) { return _collectCrudRepository.getCollectType(bangumiItem.id); } @action Future addCollect(BangumiItem bangumiItem, {type = 1}) async { if (type == 0) { await deleteCollect(bangumiItem); return; } await _collectCrudRepository.addCollectible(bangumiItem, type); final int collectChangeId = (DateTime.now().millisecondsSinceEpoch ~/ 1000); final CollectedBangumiChange collectChange = CollectedBangumiChange( collectChangeId, bangumiItem.id, 1, type, (DateTime.now().millisecondsSinceEpoch ~/ 1000)); await _collectCrudRepository.addCollectChange(collectChange); loadCollectibles(); } @action Future deleteCollect(BangumiItem bangumiItem) async { await _collectCrudRepository.deleteCollectible(bangumiItem.id); final int collectChangeId = (DateTime.now().millisecondsSinceEpoch ~/ 1000); final CollectedBangumiChange collectChange = CollectedBangumiChange( collectChangeId, bangumiItem.id, 3, 5, (DateTime.now().millisecondsSinceEpoch ~/ 1000)); await _collectCrudRepository.addCollectChange(collectChange); loadCollectibles(); } Future updateLocalCollect(BangumiItem bangumiItem) async { await _collectCrudRepository.updateCollectible(bangumiItem); loadCollectibles(); } Future syncCollectibles() async { if (!WebDav().initialized) { KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); return; } bool flag = true; try { await WebDav().ping(); } catch (e) { KazumiLogger().e('WebDav: WebDav connection failed', error: e); KazumiDialog.showToast(message: 'WebDav连接失败: $e'); flag = false; } if (!flag) { return; } try { await WebDav().syncCollectibles(); } catch (e){ KazumiDialog.showToast(message: 'WebDav同步失败 $e'); } loadCollectibles(); } // migrate collect from old version (favorites) Future migrateCollect() async { if (favorites.isNotEmpty) { int count = 0; for (BangumiItem bangumiItem in favorites) { await addCollect(bangumiItem, type: 1); count++; } await _collectCrudRepository.clearFavorites(); KazumiLogger().d('GStorage: detected $count uncategorized favorites, migrated to collectibles'); } } /// 根据收藏类型获取番剧ID集合 /// /// [type] 收藏类型 /// 返回番剧ID集合 Set getBangumiIdsByType(CollectType type) { return _collectRepository.getBangumiIdsByType(type); } /// 过滤掉指定收藏类型的番剧 /// /// [bangumiList] 原始番剧列表 /// [excludeType] 要排除的收藏类型 /// 返回过滤后的番剧列表 List filterBangumiByType( List bangumiList, CollectType excludeType) { final excludeIds = getBangumiIdsByType(excludeType); return bangumiList .where((item) => !excludeIds.contains(item.id)) .toList(); } } ================================================ FILE: lib/pages/collect/collect_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'collect_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$CollectController on _CollectController, Store { late final _$collectiblesAtom = Atom(name: '_CollectController.collectibles', context: context); @override ObservableList get collectibles { _$collectiblesAtom.reportRead(); return super.collectibles; } @override set collectibles(ObservableList value) { _$collectiblesAtom.reportWrite(value, super.collectibles, () { super.collectibles = value; }); } late final _$addCollectAsyncAction = AsyncAction('_CollectController.addCollect', context: context); @override Future addCollect(BangumiItem bangumiItem, {dynamic type = 1}) { return _$addCollectAsyncAction .run(() => super.addCollect(bangumiItem, type: type)); } late final _$deleteCollectAsyncAction = AsyncAction('_CollectController.deleteCollect', context: context); @override Future deleteCollect(BangumiItem bangumiItem) { return _$deleteCollectAsyncAction .run(() => super.deleteCollect(bangumiItem)); } @override String toString() { return ''' collectibles: ${collectibles} '''; } } ================================================ FILE: lib/pages/collect/collect_module.dart ================================================ import 'package:kazumi/pages/collect/collect_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class CollectModule extends Module { @override void routes(r) { r.child("/", child: (_) => const CollectPage()); } } ================================================ FILE: lib/pages/collect/collect_page.dart ================================================ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/pages/menu/menu.dart'; import 'package:kazumi/bean/card/bangumi_card.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:provider/provider.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; class CollectPage extends StatefulWidget { const CollectPage({super.key}); @override State createState() => _CollectPageState(); } class _CollectPageState extends State with SingleTickerProviderStateMixin { final CollectController collectController = Modular.get(); late NavigationBarState navigationBarState; TabController? tabController; bool showDelete = false; bool syncCollectiblesing = false; Box setting = GStorage.setting; void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } navigationBarState.updateSelectedIndex(0); Modular.to.navigate('/tab/popular/'); } @override void initState() { super.initState(); collectController.loadCollectibles(); tabController = TabController(vsync: this, length: tabs.length); navigationBarState = Provider.of(context, listen: false); } @override void dispose() { tabController?.dispose(); super.dispose(); } final List tabs = const [ Tab(text: '在看'), Tab(text: '想看'), Tab(text: '搁置'), Tab(text: '看过'), Tab(text: '抛弃'), ]; @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: Scaffold( appBar: SysAppBar( needTopOffset: false, toolbarHeight: 104, bottom: TabBar( controller: tabController, tabs: tabs, indicatorColor: Theme.of(context).colorScheme.primary, ), title: const Text('追番'), actions: [ IconButton( onPressed: () { setState(() { showDelete = !showDelete; }); }, icon: showDelete ? const Icon(Icons.edit_outlined) : const Icon(Icons.edit)) ], ), floatingActionButton: FloatingActionButton( onPressed: () async { bool webDavenable = await setting.get(SettingBoxKey.webDavEnable, defaultValue: false); if (!webDavenable) { KazumiDialog.showToast(message: 'webDav未启用, 同步功能不可用'); return; } if (showDelete) { KazumiDialog.showToast(message: '编辑模式无法执行同步'); return; } if (syncCollectiblesing) { return; } setState(() { syncCollectiblesing = true; }); await collectController.syncCollectibles(); setState(() { syncCollectiblesing = false; }); }, child: syncCollectiblesing ? const SizedBox( width: 32, height: 32, child: CircularProgressIndicator()) : const Icon(Icons.cloud_sync), ), body: Observer(builder: (context) { return renderBody; }), ), ); } Widget get renderBody { if (collectController.collectibles.isNotEmpty) { return TabBarView( controller: tabController, children: contentGrid(collectController.collectibles), ); } else { return const Center( child: Text('啊嘞, 没有追番的说 (´;ω;`)'), ); } } List contentGrid(List collectedBangumiList) { List gridViewList = []; List> collectedBangumiRenderItemList = List.generate(tabs.length, (_) => []); for (CollectedBangumi element in collectedBangumiList) { collectedBangumiRenderItemList[element.type - 1].add(element); } for (List list in collectedBangumiRenderItemList) { list.sort((a, b) => b.time.millisecondsSinceEpoch .compareTo(a.time.millisecondsSinceEpoch)); } int crossCount = 3; if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) { crossCount = 5; } if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) { crossCount = 6; } for (List collectedBangumiRenderItem in collectedBangumiRenderItemList) { gridViewList.add( CustomScrollView( slivers: [ SliverPadding( padding: const EdgeInsets.fromLTRB( StyleString.cardSpace, StyleString.cardSpace, StyleString.cardSpace, 0), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: StyleString.cardSpace - 2, crossAxisSpacing: StyleString.cardSpace, crossAxisCount: crossCount, mainAxisExtent: MediaQuery.of(context).size.width / crossCount / 0.65 + MediaQuery.textScalerOf(context).scale(32.0), ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return collectedBangumiRenderItem.isNotEmpty ? Stack( children: [ BangumiCardV( bangumiItem: collectedBangumiRenderItem[index] .bangumiItem, canTap: !showDelete, ), Positioned( right: 5, bottom: 5, child: showDelete ? Container( width: 40, height: 40, decoration: BoxDecoration( color: Theme.of(context) .colorScheme .secondaryContainer, shape: BoxShape.circle, ), child: CollectButton( bangumiItem: collectedBangumiRenderItem[index] .bangumiItem, color: Theme.of(context) .colorScheme .onSecondaryContainer, ), ) : Container(), ), ], ) : null; }, childCount: collectedBangumiRenderItem.isNotEmpty ? collectedBangumiRenderItem.length : 10, ), ), ), ], ), ); } return gridViewList; } } ================================================ FILE: lib/pages/download/download_controller.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/modules/danmaku/danmaku_module.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/repositories/download_repository.dart'; import 'package:kazumi/utils/background_download_service.dart'; import 'package:kazumi/utils/download_manager.dart'; import 'package:kazumi/utils/format_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/providers/video/providers.dart'; import 'package:kazumi/request/damaku.dart'; import 'package:mobx/mobx.dart'; part 'download_controller.g.dart'; class DownloadController = _DownloadController with _$DownloadController; abstract class _DownloadController with Store { final _repository = Modular.get(); final _downloadManager = Modular.get(); final _backgroundService = BackgroundDownloadService(); @observable ObservableList records = ObservableList(); final List<_ResolveRequest> _resolveQueue = []; bool _isResolving = false; bool _isBackgroundServiceInitialized = false; Future init() async { final temp = _repository.getAllRecords(); records.clear(); records.addAll(temp); // Reset any incomplete states to 'paused' on startup // This includes 'pending' because the in-memory queue is lost on restart for (final record in records) { bool changed = false; for (final entry in record.episodes.entries) { if (entry.value.status == DownloadStatus.downloading || entry.value.status == DownloadStatus.resolving || entry.value.status == DownloadStatus.pending) { entry.value.status = DownloadStatus.paused; changed = true; } } if (changed) { _repository.putRecord(record); } } // 将旧 Hive danmakuData 迁移到独立文件,防止 Hive compact 时 OOM await _migrateDanmakuDataToFiles(); _downloadManager.onProgress = _onDownloadProgress; await _initBackgroundService(); } /// 启动时将所有旧 Hive danmakuData 迁移到独立文件并清空 Hive 字段 Future _migrateDanmakuDataToFiles() async { int migratedCount = 0; for (final record in records) { bool recordChanged = false; for (final entry in record.episodes.entries) { final episode = entry.value; if (episode.danmakuData.isEmpty || episode.downloadDirectory.isEmpty) { continue; } try { // danmakuData 已经是弹幕数组的 JSON 字符串,直接拼接成新格式写入 // 避免 jsonDecode → Danmaku.fromJson × N → toJson × N → jsonEncode 的开销 final file = File(_danmakuFilePath(episode.downloadDirectory)); await file.writeAsString( '{"danDanBangumiID":${episode.danDanBangumiID},"danmakus":${episode.danmakuData}}'); episode.danmakuData = ''; recordChanged = true; migratedCount++; } catch (e) { KazumiLogger().w( 'DownloadController: danmaku migration failed for episode ${entry.key}', error: e); } } if (recordChanged) { await _repository.putRecord(record); } } if (migratedCount > 0) { KazumiLogger().i( 'DownloadController: migrated danmaku data for $migratedCount episodes'); } } Future _initBackgroundService() async { if (!_backgroundService.isSupported) return; if (_isBackgroundServiceInitialized) return; await _backgroundService.init(); _backgroundService.onPauseAll = pauseAllDownloads; _backgroundService.addTaskDataCallback(_onTaskData); _isBackgroundServiceInitialized = true; } void _onTaskData(Object data) { if (data is Map) { final action = data['action']; if (action == 'button_pressed') { _backgroundService.handleNotificationAction(data['id'] as String); } else if (action == 'navigate_to_download') { _backgroundService.handleNavigateToDownload(); } } } final Map _speeds = {}; DateTime _lastUiUpdateTime = DateTime.now(); static const _uiUpdateInterval = Duration(milliseconds: 500); void _onDownloadProgress(String recordKey, int episodeNumber, DownloadEpisode episode, double speed) { final record = _repository.getRecord(recordKey); if (record == null || !record.episodes.containsKey(episodeNumber)) { return; } _repository.updateEpisode(recordKey, episodeNumber, episode); final key = '${recordKey}_$episodeNumber'; _speeds[key] = speed; final isFinalState = episode.status == DownloadStatus.completed || episode.status == DownloadStatus.failed || episode.status == DownloadStatus.paused; final now = DateTime.now(); if (isFinalState || now.difference(_lastUiUpdateTime) >= _uiUpdateInterval) { _lastUiUpdateTime = now; refreshRecords(); _updateBackgroundNotification(); } } Future _updateBackgroundNotification() async { if (!_backgroundService.isRunning) return; final stats = _getDownloadStats(); if (stats.activeCount == 0 && stats.pendingCount == 0) { await _backgroundService.stopService(); return; } double totalSpeed = 0; for (final entry in _speeds.entries) { totalSpeed += entry.value; } await _backgroundService.updateProgress( activeCount: stats.activeCount, totalCount: stats.totalCount, overallProgress: stats.overallProgress, speedText: formatSpeed(totalSpeed), ); } Future _startBackgroundServiceIfNeeded() async { if (!_backgroundService.isSupported || _backgroundService.isRunning) return; final started = await _backgroundService.startService(); if (started) { KazumiLogger().i('DownloadController: background service started'); } } ({int activeCount, int pendingCount, int totalCount, double overallProgress}) _getDownloadStats() { int activeCount = 0; int pendingCount = 0; int totalCount = 0; double totalProgress = 0; for (final record in records) { for (final episode in record.episodes.values) { if (episode.status == DownloadStatus.downloading) { activeCount++; totalCount++; totalProgress += episode.progressPercent; } else if (episode.status == DownloadStatus.resolving || episode.status == DownloadStatus.pending) { pendingCount++; totalCount++; } } } final overallProgress = totalCount > 0 ? totalProgress / totalCount : 0.0; return ( activeCount: activeCount, pendingCount: pendingCount, totalCount: totalCount, overallProgress: overallProgress, ); } double getSpeed(int bangumiId, String pluginName, int episodeNumber) { final key = '${pluginName}_${bangumiId}_$episodeNumber'; return _speeds[key] ?? 0.0; } @action void refreshRecords() { final temp = _repository.getAllRecords(); records.clear(); records.addAll(temp); } Plugin? _findPlugin(String pluginName) { final pluginsController = Modular.get(); for (final plugin in pluginsController.pluginList) { if (plugin.name == pluginName) return plugin; } return null; } DownloadRecord? getRecord(int bangumiId, String pluginName) { return _repository.getRecordByBangumiId(bangumiId, pluginName); } DownloadEpisode? getEpisode( int bangumiId, String pluginName, int episodeNumber) { return _repository.getEpisode(bangumiId, pluginName, episodeNumber); } DownloadEpisode? getEpisodeByUrl( int bangumiId, String pluginName, String episodePageUrl) { return _repository.getEpisodeByUrl(bangumiId, pluginName, episodePageUrl); } String? getLocalVideoPath( int bangumiId, String pluginName, int episodeNumber) { final episode = _repository.getEpisode(bangumiId, pluginName, episodeNumber); return _downloadManager.getLocalVideoPath(episode); } List getCompletedEpisodes(int bangumiId, String pluginName) { return _repository.getCompletedEpisodes(bangumiId, pluginName); } /// 弹幕文件路径 String _danmakuFilePath(String downloadDirectory) { return '$downloadDirectory/danmaku.json'; } /// 从文件读取弹幕数据 /// 支持新格式 (带 danDanBangumiID 的 wrapper) 和旧格式 (纯数组) Future<({List danmakus, int danDanBangumiID})?> _readDanmakuFromFile( String downloadDirectory) async { if (downloadDirectory.isEmpty) return null; final file = File(_danmakuFilePath(downloadDirectory)); if (!await file.exists()) return null; try { final content = await file.readAsString(); final decoded = jsonDecode(content); if (decoded is List) { // 旧格式:纯弹幕数组 final danmakus = decoded.map((json) => Danmaku.fromJson(json)).toList(); return (danmakus: danmakus, danDanBangumiID: 0); } else if (decoded is Map) { // 新格式:带 danDanBangumiID 的 wrapper final danDanBangumiID = decoded['danDanBangumiID'] as int? ?? 0; final List jsonList = decoded['danmakus'] as List? ?? []; final danmakus = jsonList.map((json) => Danmaku.fromJson(json)).toList(); return (danmakus: danmakus, danDanBangumiID: danDanBangumiID); } return null; } catch (e) { KazumiLogger() .w('DownloadController: failed to read danmaku file', error: e); return null; } } /// 写入弹幕数据到文件 (新格式,包含 danDanBangumiID) Future _writeDanmakuToFile( String downloadDirectory, List danmakus, int danDanBangumiID) async { if (downloadDirectory.isEmpty) return; final file = File(_danmakuFilePath(downloadDirectory)); final wrapper = { 'danDanBangumiID': danDanBangumiID, 'danmakus': danmakus.map((d) => d.toJson()).toList(), }; await file.writeAsString(jsonEncode(wrapper)); } Future?> getCachedDanmakus( int bangumiId, String pluginName, int episodeNumber) async { final episode = _repository.getEpisode(bangumiId, pluginName, episodeNumber); if (episode == null) return null; // 从文件读取 final fromFile = await _readDanmakuFromFile(episode.downloadDirectory); if (fromFile != null && fromFile.danmakus.isNotEmpty) { return fromFile.danmakus; } return null; } Future updateCachedDanmakus( int bangumiId, String pluginName, int episodeNumber, List danmakus, int danDanBangumiID, ) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey); if (record == null) return; final episode = record.episodes[episodeNumber]; if (episode == null) return; try { // 写入独立文件而非 Hive await _writeDanmakuToFile( episode.downloadDirectory, danmakus, danDanBangumiID); // 确保 Hive 中不存储弹幕大数据 if (episode.danmakuData.isNotEmpty) { episode.danmakuData = ''; await _repository.updateEpisode(recordKey, episodeNumber, episode); } KazumiLogger().i( 'DownloadController: updated cached danmakus for episode $episodeNumber'); } catch (e) { KazumiLogger() .w('DownloadController: failed to update cached danmaku', error: e); } } Future startDownload({ required int bangumiId, required String bangumiName, required String bangumiCover, required String pluginName, required int episodeNumber, required String episodeName, required int road, required String episodePageUrl, }) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey) ?? DownloadRecord( bangumiId, bangumiName, bangumiCover, pluginName, {}, DateTime.now(), ); if (episodePageUrl.isNotEmpty) { for (final entry in record.episodes.entries) { if (entry.value.episodePageUrl == episodePageUrl) { KazumiLogger().i( 'DownloadController: episode URL already exists at position ${entry.key}, skipping'); return; } } } final episode = DownloadEpisode( episodeNumber, episodeName, road, DownloadStatus.resolving, 0.0, 0, 0, '', '', '', null, '', 0, episodePageUrl, ); record.episodes[episodeNumber] = episode; await _repository.putRecord(record); refreshRecords(); _resolveQueue.add(_ResolveRequest( recordKey: recordKey, bangumiId: bangumiId, pluginName: pluginName, episodeNumber: episodeNumber, episodePageUrl: episodePageUrl, )); _processResolveQueue(); } Future _processResolveQueue() async { if (_isResolving || _resolveQueue.isEmpty) return; _isResolving = true; while (_resolveQueue.isNotEmpty) { final request = _resolveQueue.removeAt(0); await _resolveAndEnqueue(request); } _isResolving = false; } Future _resolveAndEnqueue(_ResolveRequest request) async { final plugin = _findPlugin(request.pluginName); if (plugin == null) { _failEpisode(request.recordKey, request.episodeNumber, '找不到插件 ${request.pluginName}'); return; } final record = _repository.getRecord(request.recordKey); if (record == null) return; final episode = record.episodes[request.episodeNumber]; if (episode == null) return; if (episode.status != DownloadStatus.resolving) return; final fullUrl = plugin.buildFullUrl(request.episodePageUrl); KazumiLogger().i( 'DownloadController: resolving video URL for episode ${request.episodeNumber} from $fullUrl'); String? m3u8Url; final provider = WebViewVideoSourceProvider(); try { final source = await provider.resolve( fullUrl, useLegacyParser: plugin.useLegacyParser, timeout: const Duration(seconds: 30), ); m3u8Url = source.url; } on VideoSourceTimeoutException { KazumiLogger().w('DownloadController: WebView resolution timed out'); } on VideoSourceCancelledException { KazumiLogger().i('DownloadController: WebView resolution cancelled'); } catch (e) { KazumiLogger() .e('DownloadController: WebView resolution failed', error: e); } finally { provider.dispose(); } if (m3u8Url == null || m3u8Url.isEmpty) { _failEpisode(request.recordKey, request.episodeNumber, '解析视频源超时'); return; } KazumiLogger().i( 'DownloadController: resolved M3U8 URL for episode ${request.episodeNumber}: $m3u8Url'); // Update episode with resolved URL final freshRecord = _repository.getRecord(request.recordKey); if (freshRecord == null) return; final freshEpisode = freshRecord.episodes[request.episodeNumber]; if (freshEpisode == null) return; if (freshEpisode.status != DownloadStatus.resolving) return; freshEpisode.networkM3u8Url = m3u8Url; freshEpisode.status = DownloadStatus.downloading; await _repository.updateEpisode( request.recordKey, request.episodeNumber, freshEpisode); refreshRecords(); await _startBackgroundServiceIfNeeded(); final httpHeaders = plugin.buildHttpHeaders(); bool adBlockerEnabled = _repository.getForceAdBlocker() || plugin.adBlocker; await _downloadManager.enqueue(DownloadRequest( recordKey: request.recordKey, bangumiId: request.bangumiId, pluginName: request.pluginName, episodeNumber: request.episodeNumber, m3u8Url: m3u8Url, httpHeaders: httpHeaders, adBlockerEnabled: adBlockerEnabled, episode: freshEpisode, )); final Box setting = GStorage.setting; final bool downloadDanmaku = setting.get(SettingBoxKey.downloadDanmaku, defaultValue: true); if (downloadDanmaku) { _fetchAndCacheDanmakuAsync( request.recordKey, request.bangumiId, request.episodeNumber, ); } } void _fetchAndCacheDanmakuAsync( String recordKey, int bangumiId, int episodeNumber) { Future(() async { try { KazumiLogger().i( 'DownloadController: fetching danmaku for episode $episodeNumber (async)'); // 获取 DanDan 番剧 ID final danDanBangumiID = await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bangumiId); if (danDanBangumiID == 0) { KazumiLogger().w( 'DownloadController: failed to get DanDan bangumiID for $bangumiId'); return; } // 获取弹幕列表 final danmakus = await DanmakuRequest.getDanDanmaku(danDanBangumiID, episodeNumber); if (danmakus.isEmpty) { KazumiLogger().i( 'DownloadController: no danmaku found for episode $episodeNumber'); return; } // 等待 downloadDirectory 就绪(下载管理器处理任务后才设置) String downloadDirectory = ''; for (int i = 0; i < 10; i++) { final record = _repository.getRecord(recordKey); final episode = record?.episodes[episodeNumber]; if (episode == null) return; if (episode.status == DownloadStatus.failed || episode.status == DownloadStatus.paused) { return; } if (episode.downloadDirectory.isNotEmpty) { downloadDirectory = episode.downloadDirectory; break; } await Future.delayed(const Duration(seconds: 3)); } if (downloadDirectory.isEmpty) { KazumiLogger().w( 'DownloadController: downloadDirectory not ready for episode $episodeNumber, skipping danmaku cache'); return; } // 写入独立文件 await _writeDanmakuToFile( downloadDirectory, danmakus, danDanBangumiID); KazumiLogger().i( 'DownloadController: cached ${danmakus.length} danmakus for episode $episodeNumber'); } catch (e) { // 弹幕获取失败不影响下载 KazumiLogger() .w('DownloadController: failed to fetch danmaku', error: e); } }); } void _failEpisode(String recordKey, int episodeNumber, String message) { final record = _repository.getRecord(recordKey); if (record == null) return; final episode = record.episodes[episodeNumber]; if (episode == null) return; episode.status = DownloadStatus.failed; episode.errorMessage = message; _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); KazumiLogger() .w('DownloadController: episode $episodeNumber failed: $message'); } Future pauseDownload( int bangumiId, String pluginName, int episodeNumber) async { final recordKey = '${pluginName}_$bangumiId'; _downloadManager.pause(recordKey, episodeNumber); _resolveQueue.removeWhere( (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber); final record = _repository.getRecord(recordKey); if (record != null) { final episode = record.episodes[episodeNumber]; if (episode != null) { episode.status = DownloadStatus.paused; await _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); _updateBackgroundNotification(); } } } Future pauseAllDownloads() async { KazumiLogger().i('DownloadController: pausing all downloads'); _resolveQueue.clear(); for (final record in records) { for (final entry in record.episodes.entries) { final episode = entry.value; if (episode.status == DownloadStatus.downloading || episode.status == DownloadStatus.resolving || episode.status == DownloadStatus.pending) { final recordKey = '${record.pluginName}_${record.bangumiId}'; _downloadManager.pause(recordKey, entry.key); episode.status = DownloadStatus.paused; await _repository.updateEpisode(recordKey, entry.key, episode); } } } refreshRecords(); await _backgroundService.stopService(); } Future retryDownload({ required int bangumiId, required String pluginName, required int episodeNumber, }) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey); if (record == null) return; final episode = record.episodes[episodeNumber]; if (episode == null) return; final plugin = _findPlugin(pluginName); if (plugin == null) { _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName'); return; } // If we already have a resolved M3U8 URL, go directly to download if (episode.networkM3u8Url.isNotEmpty) { episode.status = DownloadStatus.downloading; episode.errorMessage = ''; episode.progressPercent = 0.0; episode.downloadedSegments = 0; await _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); await _startBackgroundServiceIfNeeded(); final httpHeaders = plugin.buildHttpHeaders(); bool adBlockerEnabled = _repository.getForceAdBlocker() || plugin.adBlocker; await _downloadManager.enqueue(DownloadRequest( recordKey: recordKey, bangumiId: bangumiId, pluginName: pluginName, episodeNumber: episodeNumber, m3u8Url: episode.networkM3u8Url, httpHeaders: httpHeaders, adBlockerEnabled: adBlockerEnabled, episode: episode, )); } else { episode.status = DownloadStatus.resolving; episode.errorMessage = ''; episode.progressPercent = 0.0; episode.downloadedSegments = 0; await _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); _resolveQueue.add(_ResolveRequest( recordKey: recordKey, bangumiId: bangumiId, pluginName: pluginName, episodeNumber: episodeNumber, episodePageUrl: episode.episodePageUrl, )); _processResolveQueue(); } } Future cancelDownload( int bangumiId, String pluginName, int episodeNumber) async { final recordKey = '${pluginName}_$bangumiId'; _downloadManager.cancel(recordKey, episodeNumber); _resolveQueue.removeWhere( (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber); await _downloadManager.deleteEpisodeFiles( bangumiId, pluginName, episodeNumber); await _repository.deleteEpisode(recordKey, episodeNumber); refreshRecords(); _updateBackgroundNotification(); } Future deleteRecord(int bangumiId, String pluginName) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey); if (record != null) { for (final ep in record.episodes.keys) { _downloadManager.cancel(recordKey, ep); _speeds.remove('${recordKey}_$ep'); } } _resolveQueue.removeWhere((r) => r.recordKey == recordKey); await _downloadManager.deleteRecordFiles(bangumiId, pluginName); await _repository.deleteRecord(recordKey); refreshRecords(); _updateBackgroundNotification(); } Future deleteEpisode( int bangumiId, String pluginName, int episodeNumber) async { final recordKey = '${pluginName}_$bangumiId'; _downloadManager.cancel(recordKey, episodeNumber); _speeds.remove('${recordKey}_$episodeNumber'); _resolveQueue.removeWhere( (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber); await _downloadManager.deleteEpisodeFiles( bangumiId, pluginName, episodeNumber); await _repository.deleteEpisode(recordKey, episodeNumber); refreshRecords(); _updateBackgroundNotification(); } Future priorityDownload({ required int bangumiId, required String pluginName, required int episodeNumber, }) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey); if (record == null) return; final episode = record.episodes[episodeNumber]; if (episode == null) return; final plugin = _findPlugin(pluginName); if (plugin == null) { _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName'); return; } _resolveQueue.removeWhere( (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber); if (episode.networkM3u8Url.isNotEmpty) { episode.status = DownloadStatus.downloading; episode.errorMessage = ''; await _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); await _startBackgroundServiceIfNeeded(); final httpHeaders = plugin.buildHttpHeaders(); bool adBlockerEnabled = _repository.getForceAdBlocker() || plugin.adBlocker; await _downloadManager.enqueuePriority(DownloadRequest( recordKey: recordKey, bangumiId: bangumiId, pluginName: pluginName, episodeNumber: episodeNumber, m3u8Url: episode.networkM3u8Url, httpHeaders: httpHeaders, adBlockerEnabled: adBlockerEnabled, episode: episode, )); } else { episode.status = DownloadStatus.resolving; episode.errorMessage = ''; await _repository.updateEpisode(recordKey, episodeNumber, episode); refreshRecords(); _resolveQueue.insert( 0, _ResolveRequest( recordKey: recordKey, bangumiId: bangumiId, pluginName: pluginName, episodeNumber: episodeNumber, episodePageUrl: episode.episodePageUrl, )); _processResolveQueue(); } } Future resumeAllDownloads(int bangumiId, String pluginName) async { final recordKey = '${pluginName}_$bangumiId'; final record = _repository.getRecord(recordKey); if (record == null) return; final incompleteEpisodes = record.episodes.entries .where((e) => e.value.status == DownloadStatus.paused || e.value.status == DownloadStatus.failed || e.value.status == DownloadStatus.pending) .toList() ..sort((a, b) => a.key.compareTo(b.key)); for (final entry in incompleteEpisodes) { await retryDownload( bangumiId: bangumiId, pluginName: pluginName, episodeNumber: entry.key, ); } if (incompleteEpisodes.isNotEmpty) { KazumiLogger().i( 'DownloadController: resumed ${incompleteEpisodes.length} downloads for $recordKey', ); } } int completedCount(DownloadRecord record) { return record.episodes.values .where((e) => e.status == DownloadStatus.completed) .length; } } class _ResolveRequest { final String recordKey; final int bangumiId; final String pluginName; final int episodeNumber; final String episodePageUrl; _ResolveRequest({ required this.recordKey, required this.bangumiId, required this.pluginName, required this.episodeNumber, required this.episodePageUrl, }); } ================================================ FILE: lib/pages/download/download_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'download_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$DownloadController on _DownloadController, Store { late final _$recordsAtom = Atom(name: '_DownloadController.records', context: context); @override ObservableList get records { _$recordsAtom.reportRead(); return super.records; } @override set records(ObservableList value) { _$recordsAtom.reportWrite(value, super.records, () { super.records = value; }); } late final _$_DownloadControllerActionController = ActionController(name: '_DownloadController', context: context); @override void refreshRecords() { final _$actionInfo = _$_DownloadControllerActionController.startAction( name: '_DownloadController.refreshRecords'); try { return super.refreshRecords(); } finally { _$_DownloadControllerActionController.endAction(_$actionInfo); } } @override String toString() { return ''' records: ${records} '''; } } ================================================ FILE: lib/pages/download/download_episode_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/modules/roads/road_module.dart'; import 'package:kazumi/pages/download/download_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; class DownloadEpisodeSheet extends StatefulWidget { final int road; const DownloadEpisodeSheet({super.key, required this.road}); @override State createState() => _DownloadEpisodeSheetState(); } class _DownloadEpisodeSheetState extends State { final VideoPageController videoPageController = Modular.get(); final DownloadController downloadController = Modular.get(); final Set _selectedEpisodes = {}; Road get currentRoadData => videoPageController.roadList[widget.road]; int get episodeCount => currentRoadData.data.length; @override Widget build(BuildContext context) { final record = downloadController.getRecord( videoPageController.bangumiItem.id, videoPageController.currentPlugin.name, ); final downloadedUrls = {}; if (record != null) { for (final entry in record.episodes.entries) { if (entry.value.status == DownloadStatus.completed || entry.value.status == DownloadStatus.downloading || entry.value.status == DownloadStatus.pending) { if (entry.value.episodePageUrl.isNotEmpty) { downloadedUrls.add(entry.value.episodePageUrl); } } } } return DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.3, maxChildSize: 0.9, expand: false, builder: (context, scrollController) { return Column( children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '选择要下载的集数', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( '已选 ${_selectedEpisodes.length} 集', style: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.primary, ), ), ], ), ), // Select all / deselect all Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ TextButton( onPressed: () { setState(() { _selectedEpisodes.clear(); for (int i = 1; i <= episodeCount; i++) { final url = currentRoadData.data[i - 1]; if (!downloadedUrls.contains(url)) { _selectedEpisodes.add(i); } } }); }, child: const Text('全选'), ), TextButton( onPressed: () { setState(() { _selectedEpisodes.clear(); }); }, child: const Text('取消全选'), ), ], ), ), SizedBox(height: 8), // Grid Expanded( child: GridView.builder( controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 12), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 8, mainAxisSpacing: 8, mainAxisExtent: 56, ), itemCount: episodeCount, itemBuilder: (context, index) { final episodeNumber = index + 1; final episodeUrl = currentRoadData.data[index]; final isDownloaded = downloadedUrls.contains(episodeUrl); final isSelected = _selectedEpisodes.contains(episodeNumber); final identifier = currentRoadData.identifier[index]; return Material( color: isDownloaded ? Theme.of(context) .colorScheme .surfaceContainerHighest .withValues(alpha: 0.5) : isSelected ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(8), clipBehavior: Clip.hardEdge, child: InkWell( onTap: isDownloaded ? null : () { setState(() { if (isSelected) { _selectedEpisodes.remove(episodeNumber); } else { _selectedEpisodes.add(episodeNumber); } }); }, child: Stack( children: [ Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( identifier, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: isDownloaded ? Theme.of(context).colorScheme.outline : null, ), ), ), ), if (isDownloaded) Positioned( top: 4, right: 4, child: Icon( Icons.check_circle, size: 14, color: Theme.of(context).colorScheme.outline, ), ), if (isSelected) Positioned( top: 4, right: 4, child: Icon( Icons.check_circle, size: 14, color: Theme.of(context).colorScheme.primary, ), ), ], ), ), ); }, ), ), // Action buttons Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: SafeArea( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), const SizedBox(width: 12), SizedBox( width: 140, child: FilledButton( onPressed: _selectedEpisodes.isEmpty ? null : () => _startBatchDownload(context), child: Text('开始下载(${_selectedEpisodes.length})'), ), ), ], ), ), ), ], ); }, ); } void _startBatchDownload(BuildContext context) { Navigator.pop(context); final plugin = videoPageController.currentPlugin; final bangumiItem = videoPageController.bangumiItem; final sortedEpisodes = _selectedEpisodes.toList()..sort(); for (final episodeNumber in sortedEpisodes) { final episodePageUrl = currentRoadData.data[episodeNumber - 1]; final identifier = currentRoadData.identifier[episodeNumber - 1]; downloadController.startDownload( bangumiId: bangumiItem.id, bangumiName: bangumiItem.nameCn.isNotEmpty ? bangumiItem.nameCn : bangumiItem.name, bangumiCover: bangumiItem.images['large'] ?? '', pluginName: plugin.name, episodeNumber: episodeNumber, episodeName: identifier, road: widget.road, episodePageUrl: episodePageUrl, ); } KazumiDialog.showToast( message: '已添加 ${sortedEpisodes.length} 集到下载队列,可在下载管理中查看', ); } } ================================================ FILE: lib/pages/download/download_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/pages/download/download_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/utils/format_utils.dart'; class DownloadPage extends StatefulWidget { const DownloadPage({super.key}); @override State createState() => _DownloadPageState(); } class _DownloadPageState extends State { final DownloadController downloadController = Modular.get(); @override void initState() { super.initState(); downloadController.refreshRecords(); } @override Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar(title: Text('下载管理')), body: Observer(builder: (context) { if (downloadController.records.isEmpty) { return const Center( child: Text('暂无离线下载'), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: downloadController.records.length, itemBuilder: (context, index) { final record = downloadController.records[index]; return _buildRecordCard(record); }, ); }), ); } Widget _buildRecordCard(DownloadRecord record) { final episodes = record.episodes.values.toList() ..sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber)); final completedCount = downloadController.completedCount(record); return Card( margin: const EdgeInsets.only(bottom: 12), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(4), child: Image.network( record.bangumiCover, width: 48, height: 64, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: 48, height: 64, color: Theme.of(context).colorScheme.surfaceContainerHighest, child: const Icon(Icons.movie_outlined), ), ), ), title: Text( record.bangumiName, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( '来源: ${record.pluginName} · $completedCount/${episodes.length} 已完成', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline, ), ), trailing: PopupMenuButton( onSelected: (value) { if (value == 'delete') { _confirmDeleteRecord(record); } else if (value == 'resume_all') { downloadController.resumeAllDownloads( record.bangumiId, record.pluginName, ); KazumiDialog.showToast(message: '已开始恢复下载'); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'resume_all', child: Text('开始全部'), ), const PopupMenuItem( value: 'delete', child: Text('删除全部'), ), ], ), ), // Episode list ...episodes.map((ep) => _buildEpisodeTile(record, ep)), ], ), ); } Widget _buildEpisodeTile(DownloadRecord record, DownloadEpisode episode) { final statusIcon = _getStatusIcon(episode); final statusText = _getStatusText(record, episode); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Row( children: [ statusIcon, const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( episode.episodeName.isNotEmpty ? episode.episodeName : '第${episode.episodeNumber}集', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14), ), const SizedBox(height: 2), Text( statusText, style: TextStyle( fontSize: 12, color: episode.status == DownloadStatus.failed ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.outline, ), ), ], ), ), if (episode.status == DownloadStatus.downloading) ...[ SizedBox( width: 60, child: LinearProgressIndicator( value: episode.progressPercent, minHeight: 3, ), ), const SizedBox(width: 8), ], ..._getActionButtons(record, episode), ], ), ); } Widget _getStatusIcon(DownloadEpisode episode) { switch (episode.status) { case DownloadStatus.completed: return Icon(Icons.offline_pin, size: 20, color: Theme.of(context).colorScheme.primary); case DownloadStatus.downloading: return SizedBox( width: 20, height: 20, child: CircularProgressIndicator( value: episode.progressPercent, strokeWidth: 2, ), ); case DownloadStatus.failed: return Icon(Icons.error_outline, size: 20, color: Theme.of(context).colorScheme.error); case DownloadStatus.paused: return Icon(Icons.pause_circle_outline, size: 20, color: Theme.of(context).colorScheme.outline); case DownloadStatus.pending: return Icon(Icons.hourglass_empty, size: 20, color: Theme.of(context).colorScheme.outline); case DownloadStatus.resolving: return const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ); default: return const SizedBox(width: 20, height: 20); } } String _getStatusText(DownloadRecord record, DownloadEpisode episode) { switch (episode.status) { case DownloadStatus.completed: return '已完成 ${formatBytes(episode.totalBytes)}'; case DownloadStatus.downloading: final speed = downloadController.getSpeed( record.bangumiId, record.pluginName, episode.episodeNumber, ); final speedText = speed > 0 ? ' · ${formatSpeed(speed)}' : ''; return '${(episode.progressPercent * 100).toStringAsFixed(0)}% ' '${episode.downloadedSegments}/${episode.totalSegments}$speedText'; case DownloadStatus.failed: return episode.errorMessage.isNotEmpty ? episode.errorMessage : '下载失败'; case DownloadStatus.paused: return '已暂停 ${(episode.progressPercent * 100).toStringAsFixed(0)}%'; case DownloadStatus.pending: return '等待中'; case DownloadStatus.resolving: return '解析视频源中'; default: return ''; } } List _getActionButtons( DownloadRecord record, DownloadEpisode episode) { final buttons = []; switch (episode.status) { case DownloadStatus.completed: buttons.add(IconButton( icon: Icon(Icons.play_circle_outline, size: 20, color: Theme.of(context).colorScheme.primary), onPressed: () => _playEpisode(record, episode), tooltip: '播放', visualDensity: VisualDensity.compact, )); break; case DownloadStatus.downloading: buttons.add(IconButton( icon: const Icon(Icons.pause, size: 20), onPressed: () => downloadController.pauseDownload( record.bangumiId, record.pluginName, episode.episodeNumber, ), tooltip: '暂停', visualDensity: VisualDensity.compact, )); break; case DownloadStatus.paused: buttons.add(IconButton( icon: const Icon(Icons.play_arrow, size: 20), onPressed: () => downloadController.retryDownload( bangumiId: record.bangumiId, pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ), tooltip: '继续', visualDensity: VisualDensity.compact, )); break; case DownloadStatus.failed: buttons.add(IconButton( icon: const Icon(Icons.refresh, size: 20), onPressed: () => downloadController.retryDownload( bangumiId: record.bangumiId, pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ), tooltip: '重试', visualDensity: VisualDensity.compact, )); break; case DownloadStatus.pending: buttons.add(IconButton( icon: Icon(Icons.priority_high, size: 20, color: Theme.of(context).colorScheme.primary), onPressed: () { downloadController.priorityDownload( bangumiId: record.bangumiId, pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ); KazumiDialog.showToast(message: '已插队优先下载'); }, tooltip: '优先下载', visualDensity: VisualDensity.compact, )); break; default: break; } buttons.add(IconButton( icon: const Icon(Icons.delete_outline, size: 20), onPressed: () => _confirmDeleteEpisode(record, episode), tooltip: '删除', visualDensity: VisualDensity.compact, )); return buttons; } void _playEpisode(DownloadRecord record, DownloadEpisode episode) { final localPath = downloadController.getLocalVideoPath( record.bangumiId, record.pluginName, episode.episodeNumber, ); if (localPath == null) { KazumiDialog.showToast(message: '本地文件不存在'); return; } // 构建 BangumiItem final bangumiItem = BangumiItem( id: record.bangumiId, type: 2, name: record.bangumiName, nameCn: record.bangumiName, summary: '', airDate: '', airWeekday: 0, rank: 0, images: {'large': record.bangumiCover}, tags: [], alias: [], ratingScore: 0.0, votes: 0, votesCount: [], info: '', ); // 获取所有已下载集数(通过 Controller 委托给 Repository 层) final downloadedEpisodes = downloadController.getCompletedEpisodes( record.bangumiId, record.pluginName, ); // 初始化离线模式 final videoPageController = Modular.get(); videoPageController.initForOfflinePlayback( bangumiItem: bangumiItem, pluginName: record.pluginName, episodeNumber: episode.episodeNumber, episodeName: episode.episodeName, road: episode.road, videoPath: localPath, downloadedEpisodes: downloadedEpisodes, ); // 导航到 VideoPage Modular.to.pushNamed('/video/'); } void _confirmDeleteEpisode(DownloadRecord record, DownloadEpisode episode) { KazumiDialog.show( builder: (context) => AlertDialog( title: const Text('删除下载'), content: Text('确定要删除「${episode.episodeName.isNotEmpty ? episode.episodeName : '第${episode.episodeNumber}集'}」的下载文件吗?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { downloadController.deleteEpisode( record.bangumiId, record.pluginName, episode.episodeNumber, ); KazumiDialog.dismiss(); }, child: const Text('删除'), ), ], ), ); } void _confirmDeleteRecord(DownloadRecord record) { KazumiDialog.show( builder: (context) => AlertDialog( title: const Text('删除全部下载'), content: Text('确定要删除「${record.bangumiName}」的所有下载文件吗?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { downloadController.deleteRecord( record.bangumiId, record.pluginName, ); KazumiDialog.dismiss(); }, child: const Text('删除'), ), ], ), ); } } ================================================ FILE: lib/pages/download/download_page_module.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/download/download_page.dart'; class DownloadModule extends Module { @override void routes(r) { r.child("/", child: (_) => const DownloadPage()); } } ================================================ FILE: lib/pages/error/storage_error_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'package:path_provider/path_provider.dart'; class StorageErrorPage extends StatelessWidget { const StorageErrorPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('内部错误'), ), body: Center( child: FutureBuilder( future: getApplicationSupportDirectory(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final supportDir = snapshot.data; final path = supportDir != null ? '$supportDir' : '未知路径'; return GeneralErrorWidget( errMsg: '存储初始化错误 \n 当前储存位置 $path \n 尝试删除该目录以重置本地存储', actions: [ GeneralErrorButton( onPressed: () { exit(0); }, text: '退出程序', ), ], ); } else { return const CircularProgressIndicator(); } }, ), ), ); } } ================================================ FILE: lib/pages/history/history_controller.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/repositories/history_repository.dart'; import 'package:mobx/mobx.dart'; part 'history_controller.g.dart'; class HistoryController = _HistoryController with _$HistoryController; abstract class _HistoryController with Store { final _historyRepository = Modular.get(); @observable ObservableList histories = ObservableList(); void init() { final temp = _historyRepository.getAllHistories(); histories.clear(); histories.addAll(temp); } Future updateHistory( int episode, int road, String adapterName, BangumiItem bangumiItem, Duration progress, String lastSrc, String lastWatchEpisodeName) async { await _historyRepository.updateHistory( episode: episode, road: road, adapterName: adapterName, bangumiItem: bangumiItem, progress: progress, lastSrc: lastSrc, lastWatchEpisodeName: lastWatchEpisodeName, ); init(); } Progress? lastWatching(BangumiItem bangumiItem, String adapterName) { return _historyRepository.getLastWatchingProgress(bangumiItem, adapterName); } Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode) { return _historyRepository.findProgress(bangumiItem, adapterName, episode); } Future deleteHistory(History history) async { await _historyRepository.deleteHistory(history); init(); } Future clearProgress(BangumiItem bangumiItem, String adapterName, int episode) async { await _historyRepository.clearProgress(bangumiItem, adapterName, episode); init(); } Future clearAll() async { await _historyRepository.clearAllHistories(); histories.clear(); } } ================================================ FILE: lib/pages/history/history_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'history_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$HistoryController on _HistoryController, Store { late final _$historiesAtom = Atom(name: '_HistoryController.histories', context: context); @override ObservableList get histories { _$historiesAtom.reportRead(); return super.histories; } @override set histories(ObservableList value) { _$historiesAtom.reportWrite(value, super.histories, () { super.histories = value; }); } @override String toString() { return ''' histories: ${histories} '''; } } ================================================ FILE: lib/pages/history/history_module.dart ================================================ import 'package:kazumi/pages/history/history_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class HistoryModule extends Module { @override void routes(r) { r.child("/", child: (_) => const HistoryPage()); } } ================================================ FILE: lib/pages/history/history_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/bean/card/bangumi_history_card.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/utils/constants.dart'; class HistoryPage extends StatefulWidget { const HistoryPage({super.key}); @override State createState() => _HistoryPageState(); } class _HistoryPageState extends State with SingleTickerProviderStateMixin { final HistoryController historyController = Modular.get(); /// show delete button bool showDelete = false; @override void initState() { super.initState(); historyController.init(); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } void showHistoryClearDialog() { KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('记录管理'), content: const Text('确认要清除所有历史记录吗?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { KazumiDialog.dismiss(); try { historyController.clearAll(); } catch (_) {} }, child: const Text('确认'), ), ], ); }, ); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) {}); return Observer(builder: (context) { return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) async { onBackPressed(context); }, child: Scaffold( appBar: SysAppBar( title: const Text('历史记录'), actions: [ IconButton( onPressed: () { setState(() { showDelete = !showDelete; }); }, icon: showDelete ? const Icon(Icons.edit_outlined) : const Icon(Icons.edit)) ], ), body: SafeArea(bottom: false, child: renderBody), floatingActionButton: FloatingActionButton( child: const Icon(Icons.clear_all), onPressed: () { showHistoryClearDialog(); }, ), ), ); }); } Widget get renderBody { if (historyController.histories.isNotEmpty) { return contentGrid; } else { return const Center( child: Text('没有找到历史记录 (´;ω;`)'), ); } } Widget get contentGrid { int crossCount = 1; if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) { crossCount = 2; } if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) { crossCount = 3; } double cardHeight = 120; return CustomScrollView( slivers: [ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: StyleString.cardSpace - 2, crossAxisSpacing: StyleString.cardSpace, crossAxisCount: crossCount, mainAxisExtent: cardHeight + 12, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return historyController.histories.isNotEmpty ? BangumiHistoryCardV( showDelete: showDelete, cardHeight: cardHeight, historyItem: historyController.histories[index]) : null; }, childCount: historyController.histories.isNotEmpty ? historyController.histories.length : 10, ), ), ], ); } } ================================================ FILE: lib/pages/index_module.dart ================================================ import 'package:kazumi/pages/index_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/router.dart'; import 'package:kazumi/pages/init_page.dart'; import 'package:flutter/material.dart'; import 'package:kazumi/pages/popular/popular_controller.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/pages/timeline/timeline_controller.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:kazumi/pages/my/my_controller.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/pages/video/video_module.dart'; import 'package:kazumi/pages/info/info_module.dart'; import 'package:kazumi/pages/settings/settings_module.dart'; import 'package:kazumi/shaders/shaders_controller.dart'; import 'package:kazumi/pages/search/search_module.dart'; import 'package:kazumi/repositories/collect_repository.dart'; import 'package:kazumi/repositories/search_history_repository.dart'; import 'package:kazumi/repositories/collect_crud_repository.dart'; import 'package:kazumi/repositories/history_repository.dart'; import 'package:kazumi/repositories/download_repository.dart'; import 'package:kazumi/utils/download_manager.dart'; import 'package:kazumi/pages/download/download_controller.dart'; class IndexModule extends Module { @override List get imports => menu.moduleList; @override void binds(i) { // Repository层 i.addSingleton(CollectRepository.new); i.addSingleton(SearchHistoryRepository.new); i.addSingleton(CollectCrudRepository.new); i.addSingleton(HistoryRepository.new); i.addSingleton(DownloadRepository.new); i.addSingleton(DownloadManager.new); // Controller层 i.addSingleton(PopularController.new); i.addSingleton(PluginsController.new); i.addSingleton(VideoPageController.new); i.addSingleton(TimelineController.new); i.addSingleton(CollectController.new); i.addSingleton(HistoryController.new); i.addSingleton(MyController.new); i.addSingleton(ShadersController.new); i.addSingleton(DownloadController.new); } @override void routes(r) { r.child("/", child: (_) => const InitPage(), children: [ ChildRoute( "/error", child: (_) => Scaffold( appBar: AppBar(title: const Text("Kazumi")), body: const Center(child: Text("初始化失败")), ), ), ], transition: TransitionType.noTransition); r.child( "/tab", child: (_) { return const IndexPage(); }, children: menu.routes, transition: TransitionType.fadeIn, duration: Duration(milliseconds: 70), ); r.module("/video", module: VideoModule()); /// The route need [ BangumiItem ] as argument. r.module("/info", module: InfoModule()); r.module("/settings", module: SettingsModule()); r.module("/search", module: SearchModule()); } } ================================================ FILE: lib/pages/index_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/pages/menu/menu.dart'; class IndexPage extends StatefulWidget { //const IndexPage({super.key}); const IndexPage({super.key}); @override State createState() => _IndexPageState(); } class _IndexPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return const ScaffoldMenu(); } } ================================================ FILE: lib/pages/info/character_page.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:kazumi/modules/character/character_full_item.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:kazumi/bean/card/character_comments_card.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; class CharacterPage extends StatefulWidget { const CharacterPage({super.key, required this.characterID}); final int characterID; @override State createState() => _CharacterPageState(); } class _CharacterPageState extends State { late CharacterFullItem characterFullItem; bool loadingCharacter = true; List commentsList = []; bool loadingComments = true; Future loadCharacter() async { setState(() { loadingCharacter = true; }); await BangumiHTTP.getCharacterByCharacterID(widget.characterID) .then((character) { characterFullItem = character; }); if (mounted) { setState(() { loadingCharacter = false; }); } } Future loadComments() async { setState(() { loadingComments = true; }); await BangumiHTTP.getCharacterCommentsByCharacterID(widget.characterID) .then((value) { commentsList = value.commentList; }); if (mounted) { setState(() { loadingComments = false; }); } } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { loadCharacter(); loadComments(); }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( body: Column( children: [ const PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Material( child: TabBar( tabs: [ Tab(text: '人物资料'), Tab(text: '吐槽箱'), ], ), ), ), Expanded( child: TabBarView( children: [characterInfoBody, characterCommentsBody], ), ), ], ), ), ); } Widget get characterInfoBody { return Padding( padding: const EdgeInsets.all(8.0), child: LayoutBuilder(builder: (context, constraints) { return Column( children: [ Expanded( child: loadingCharacter ? const Center(child: CircularProgressIndicator()) : (characterFullItem.id == 0 ? GeneralErrorWidget( errMsg: '什么都没有找到 (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { loadCharacter(); }, text: '点击重试', ), ], ) : SizedBox( width: double.infinity, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: constraints.maxWidth * 0.3, height: constraints.maxHeight, child: NetworkImgLayer( width: constraints.maxWidth, height: constraints.maxHeight, src: characterFullItem.image, ), ), Expanded( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( characterFullItem.name, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context) .colorScheme .tertiary, ), overflow: TextOverflow.ellipsis, maxLines: 2, ), Padding( padding: const EdgeInsets.only( top: 4.0, bottom: 12.0), child: Text( characterFullItem.nameCN, style: Theme.of(context) .textTheme .titleMedium ?.copyWith( color: Colors.grey[700], ), ), ), const Divider(), Padding( padding: const EdgeInsets.symmetric( vertical: 8.0), child: Text( '基本信息', style: Theme.of(context) .textTheme .titleSmall ?.copyWith( fontWeight: FontWeight.bold, ), ), ), Text( characterFullItem.info, style: Theme.of(context) .textTheme .bodyMedium, textAlign: TextAlign.justify, ), const SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric( vertical: 8.0), child: Text( '角色简介', style: Theme.of(context) .textTheme .titleSmall ?.copyWith( fontWeight: FontWeight.bold, ), ), ), Text( characterFullItem.summary, style: Theme.of(context) .textTheme .bodyMedium, textAlign: TextAlign.justify, ), ], ), ), ), ), ], ), )), ), ], ); }), ); } Widget get characterCommentsBody { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( // Scrollbars' movement is not linear so hide it. scrollbars: false, // Enable mouse drag to refresh dragDevices: { PointerDeviceKind.mouse, PointerDeviceKind.touch, }, ), slivers: [ SliverPadding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 4), sliver: Builder(builder: (context) { if (loadingComments) { return const SliverFillRemaining( child: Center( child: CircularProgressIndicator(), ), ); } if (commentsList.isEmpty) { return SliverFillRemaining( child: GeneralErrorWidget( errMsg: '什么都没有找到 (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { loadComments(); }, text: '点击重试', ), ], ), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { // Fix scroll issue caused by height change of network images // by keeping loaded cards alive. return KeepAlive( keepAlive: true, child: IndexedSemantics( index: index, child: SelectionArea( child: CharacterCommentsCard( commentItem: commentsList[index], ), ), ), ); }, childCount: commentsList.length, addAutomaticKeepAlives: false, addRepaintBoundaries: false, addSemanticIndexes: false, ), ); }), ), ], ); } } ================================================ FILE: lib/pages/info/info_controller.dart ================================================ import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/modules/search/plugin_search_module.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:mobx/mobx.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/modules/characters/character_item.dart'; import 'package:kazumi/modules/staff/staff_item.dart'; part 'info_controller.g.dart'; class InfoController = _InfoController with _$InfoController; abstract class _InfoController with Store { final CollectController collectController = Modular.get(); late BangumiItem bangumiItem; @observable bool isLoading = false; @observable var pluginSearchResponseList = ObservableList(); @observable var pluginSearchStatus = ObservableMap(); @observable var commentsList = ObservableList(); @observable var characterList = ObservableList(); @observable var staffList = ObservableList(); Future queryBangumiInfoByID(int id, {String type = "init"}) async { isLoading = true; await BangumiHTTP.getBangumiInfoByID(id).then((value) { if (value != null) { if (type == "init") { bangumiItem = value; } else { bangumiItem.summary = value.summary; bangumiItem.tags = value.tags; bangumiItem.rank = value.rank; bangumiItem.airDate = value.airDate; bangumiItem.airWeekday = value.airWeekday; bangumiItem.alias = value.alias; bangumiItem.ratingScore = value.ratingScore; bangumiItem.votes = value.votes; bangumiItem.votesCount = value.votesCount; } collectController.updateLocalCollect(bangumiItem); isLoading = false; } }); } Future queryBangumiCommentsByID(int id, {int offset = 0}) async { if (offset == 0) { commentsList.clear(); } await BangumiHTTP.getBangumiCommentsByID(id, offset: offset).then((value) { commentsList.addAll(value.commentList); }); KazumiLogger().i('InfoController: loaded comments list length ${commentsList.length}'); } Future queryBangumiCharactersByID(int id) async { characterList.clear(); await BangumiHTTP.getCharatersByBangumiID(id).then((value) { characterList.addAll(value.charactersList); }); Map relationValue = { '主角': 1, '配角': 2, '客串': 3, }; try { characterList.sort((a, b) { int valueA = relationValue[a.relation] ?? 4; int valueB = relationValue[b.relation] ?? 4; return valueA.compareTo(valueB); }); } catch (e) { KazumiDialog.showToast(message: '$e'); } KazumiLogger().i('InfoController: loaded character list length ${characterList.length}'); } Future queryBangumiStaffsByID(int id) async { staffList.clear(); await BangumiHTTP.getBangumiStaffByID(id).then((value) { staffList.addAll(value.data); }); KazumiLogger().i('InfoController: loaded staff list length ${staffList.length}'); } } ================================================ FILE: lib/pages/info/info_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'info_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$InfoController on _InfoController, Store { late final _$isLoadingAtom = Atom(name: '_InfoController.isLoading', context: context); @override bool get isLoading { _$isLoadingAtom.reportRead(); return super.isLoading; } @override set isLoading(bool value) { _$isLoadingAtom.reportWrite(value, super.isLoading, () { super.isLoading = value; }); } late final _$pluginSearchResponseListAtom = Atom(name: '_InfoController.pluginSearchResponseList', context: context); @override ObservableList get pluginSearchResponseList { _$pluginSearchResponseListAtom.reportRead(); return super.pluginSearchResponseList; } @override set pluginSearchResponseList(ObservableList value) { _$pluginSearchResponseListAtom .reportWrite(value, super.pluginSearchResponseList, () { super.pluginSearchResponseList = value; }); } late final _$pluginSearchStatusAtom = Atom(name: '_InfoController.pluginSearchStatus', context: context); @override ObservableMap get pluginSearchStatus { _$pluginSearchStatusAtom.reportRead(); return super.pluginSearchStatus; } @override set pluginSearchStatus(ObservableMap value) { _$pluginSearchStatusAtom.reportWrite(value, super.pluginSearchStatus, () { super.pluginSearchStatus = value; }); } late final _$commentsListAtom = Atom(name: '_InfoController.commentsList', context: context); @override ObservableList get commentsList { _$commentsListAtom.reportRead(); return super.commentsList; } @override set commentsList(ObservableList value) { _$commentsListAtom.reportWrite(value, super.commentsList, () { super.commentsList = value; }); } late final _$characterListAtom = Atom(name: '_InfoController.characterList', context: context); @override ObservableList get characterList { _$characterListAtom.reportRead(); return super.characterList; } @override set characterList(ObservableList value) { _$characterListAtom.reportWrite(value, super.characterList, () { super.characterList = value; }); } late final _$staffListAtom = Atom(name: '_InfoController.staffList', context: context); @override ObservableList get staffList { _$staffListAtom.reportRead(); return super.staffList; } @override set staffList(ObservableList value) { _$staffListAtom.reportWrite(value, super.staffList, () { super.staffList = value; }); } @override String toString() { return ''' isLoading: ${isLoading}, pluginSearchResponseList: ${pluginSearchResponseList}, pluginSearchStatus: ${pluginSearchStatus}, commentsList: ${commentsList}, characterList: ${characterList}, staffList: ${staffList} '''; } } ================================================ FILE: lib/pages/info/info_module.dart ================================================ import 'package:kazumi/pages/info/info_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class InfoModule extends Module { @override void routes(r) { r.child("/", child: (_) => const InfoPage()); } } ================================================ FILE: lib/pages/info/info_page.dart ================================================ import 'dart:io'; import 'dart:ui'; import 'package:kazumi/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/pages/info/info_controller.dart'; import 'package:kazumi/bean/card/bangumi_info_card.dart'; import 'package:kazumi/pages/info/source_sheet.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/pages/info/info_tabview.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb; class InfoPage extends StatefulWidget { const InfoPage({super.key}); @override State createState() => _InfoPageState(); } class _InfoPageState extends State with TickerProviderStateMixin { /// Don't use modular singleton here. We may have multiple info pages. /// Use a new instance of InfoController for each info page. final InfoController infoController = InfoController(); final VideoPageController videoPageController = Modular.get(); final PluginsController pluginsController = Modular.get(); late TabController sourceTabController; late TabController infoTabController; late bool showRating; bool commentsIsLoading = false; bool charactersIsLoading = false; bool commentsQueryTimeout = false; bool charactersQueryTimeout = false; bool staffIsLoading = false; bool staffQueryTimeout = false; final inputBangumiIten = Modular.args.data as BangumiItem; Future loadCharacters() async { if (charactersIsLoading) return; setState(() { charactersIsLoading = true; charactersQueryTimeout = false; }); infoController .queryBangumiCharactersByID(infoController.bangumiItem.id) .then((_) { if (infoController.characterList.isEmpty && mounted) { setState(() { charactersIsLoading = false; charactersQueryTimeout = true; }); } if (infoController.characterList.isNotEmpty && mounted) { setState(() { charactersIsLoading = false; }); } }); } Future loadStaff() async { if (staffIsLoading) return; setState(() { staffIsLoading = true; staffQueryTimeout = false; }); infoController .queryBangumiStaffsByID(infoController.bangumiItem.id) .then((_) { if (infoController.staffList.isEmpty && mounted) { setState(() { staffIsLoading = false; staffQueryTimeout = true; }); } if (infoController.staffList.isNotEmpty && mounted) { setState(() { staffIsLoading = false; }); } }); } Future loadMoreComments({int offset = 0}) async { if (commentsIsLoading) return; setState(() { commentsIsLoading = true; commentsQueryTimeout = false; }); infoController .queryBangumiCommentsByID(infoController.bangumiItem.id, offset: offset) .then((_) { if (infoController.commentsList.isEmpty && mounted) { setState(() { commentsIsLoading = false; commentsQueryTimeout = true; }); } if (infoController.commentsList.isNotEmpty && mounted) { setState(() { commentsIsLoading = false; }); } }); } @override void initState() { super.initState(); infoController.bangumiItem = inputBangumiIten; infoController.characterList.clear(); infoController.commentsList.clear(); infoController.staffList.clear(); infoController.pluginSearchResponseList.clear(); videoPageController.currentEpisode = 1; // Because the gap between different bangumi API response is too large, sometimes we need to query the bangumi info again // We need the type parameter to determine whether to attach the new data to the old data // 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 if (infoController.bangumiItem.summary == '' || infoController.bangumiItem.votesCount.isEmpty) { queryBangumiInfoByID(infoController.bangumiItem.id, type: 'attach'); } sourceTabController = TabController(length: pluginsController.pluginList.length, vsync: this); infoTabController = TabController(length: 5, vsync: this); showRating = GStorage.setting.get(SettingBoxKey.showRating, defaultValue: true); infoTabController.addListener(() { int index = infoTabController.index; if (index == 1 && infoController.commentsList.isEmpty && !commentsIsLoading) { loadMoreComments(); } if (index == 2 && infoController.characterList.isEmpty && !charactersIsLoading) { loadCharacters(); } if (index == 4 && infoController.staffList.isEmpty && !staffIsLoading) { loadStaff(); } }); } @override void dispose() { infoController.characterList.clear(); infoController.commentsList.clear(); infoController.staffList.clear(); infoController.pluginSearchResponseList.clear(); videoPageController.currentEpisode = 1; sourceTabController.dispose(); infoTabController.dispose(); super.dispose(); } Future queryBangumiInfoByID(int id, {String type = "init"}) async { try { await infoController.queryBangumiInfoByID(id, type: type); setState(() {}); } catch (e) { KazumiLogger().e('InfoController: failed to query bangumi info by ID', error: e); } } @override Widget build(BuildContext context) { final List tabs = ['概览', '吐槽', '角色', '评论', '制作人员']; final bool showWindowButton = GStorage.setting .get(SettingBoxKey.showWindowButton, defaultValue: false); return PopScope( canPop: true, child: DefaultTabController( length: tabs.length, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar.medium( title: EmbeddedNativeControlArea( child: dtb.DragToMoveArea( child: Container( width: double.infinity, alignment: Alignment.centerLeft, child: Text( infoController.bangumiItem.nameCn == '' ? infoController.bangumiItem.name : infoController.bangumiItem.nameCn, ), ), ), ), automaticallyImplyLeading: false, scrolledUnderElevation: 0.0, leading: EmbeddedNativeControlArea( child: IconButton( onPressed: () { Navigator.maybePop(context); }, icon: Icon(Icons.arrow_back), ), ), actions: [ if (innerBoxIsScrolled) EmbeddedNativeControlArea( child: CollectButton( bangumiItem: infoController.bangumiItem, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), EmbeddedNativeControlArea( child: IconButton( onPressed: () { launchUrl( Uri.parse( 'https://bangumi.tv/subject/${infoController.bangumiItem.id}'), mode: LaunchMode.externalApplication, ); }, icon: const Icon(Icons.open_in_browser_rounded), ), ), if (!showWindowButton && Utils.isDesktop()) CloseButton(onPressed: () => windowManager.close()), SizedBox(width: 8), ], toolbarHeight: (Platform.isMacOS && showWindowButton) ? kToolbarHeight + 22 : kToolbarHeight, stretch: true, centerTitle: false, expandedHeight: (Platform.isMacOS && showWindowButton) ? 308 + kTextTabBarHeight + kToolbarHeight + 22 : 308 + kTextTabBarHeight + kToolbarHeight, collapsedHeight: (Platform.isMacOS && showWindowButton) ? kTextTabBarHeight + kToolbarHeight + MediaQuery.paddingOf(context).top + 22 : kTextTabBarHeight + kToolbarHeight + MediaQuery.paddingOf(context).top, flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.pin, background: Observer(builder: (context) { return Stack( children: [ // No background image when loading to make loading looks better if (!infoController.isLoading) Positioned.fill( bottom: kTextTabBarHeight, child: IgnorePointer( child: Opacity( opacity: 0.4, child: LayoutBuilder( builder: (context, boxConstraints) { return ImageFiltered( imageFilter: ImageFilter.blur( sigmaX: 15.0, sigmaY: 15.0), child: ShaderMask( shaderCallback: (Rect bounds) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white, Colors.transparent, ], stops: [0.8, 1], ).createShader(bounds); }, child: NetworkImgLayer( src: infoController.bangumiItem .images['large'] ?? '', width: boxConstraints.maxWidth, height: boxConstraints.maxHeight, fadeInDuration: const Duration( milliseconds: 0), fadeOutDuration: const Duration( milliseconds: 0), ), ), ); }, ), ), ), ), SafeArea( bottom: false, child: EmbeddedNativeControlArea( child: Align( alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.fromLTRB( 16, kToolbarHeight, 16, 0), child: BangumiInfoCardV( bangumiItem: infoController.bangumiItem, isLoading: infoController.isLoading, showRating: showRating, ), ), ), ), ), ], ); }), ), forceElevated: innerBoxIsScrolled, bottom: TabBar( controller: infoTabController, isScrollable: true, tabAlignment: TabAlignment.center, dividerHeight: 0, tabs: tabs.map((name) => Tab(text: name)).toList(), ), ), ), ]; }, body: Observer(builder: (context) { return InfoTabView( tabController: infoTabController, bangumiItem: infoController.bangumiItem, commentsQueryTimeout: commentsQueryTimeout, charactersQueryTimeout: charactersQueryTimeout, staffQueryTimeout: staffQueryTimeout, loadMoreComments: loadMoreComments, loadCharacters: loadCharacters, loadStaff: loadStaff, commentsList: infoController.commentsList, characterList: infoController.characterList, staffList: infoController.staffList, isLoading: infoController.isLoading, ); }), ), floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.play_arrow_rounded), label: Text('开始观看'), onPressed: () async { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: (MediaQuery.sizeOf(context).height >= LayoutBreakpoint.compact['height']!) ? MediaQuery.of(context).size.height * 3 / 4 : MediaQuery.of(context).size.height, maxWidth: (MediaQuery.sizeOf(context).width >= LayoutBreakpoint.medium['width']!) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width, ), clipBehavior: Clip.antiAlias, backgroundColor: Theme.of(context).scaffoldBackgroundColor, showDragHandle: true, context: context, builder: (context) { return SourceSheet(tabController: sourceTabController, infoController: infoController); }, ); }, ), ), ), ); } } ================================================ FILE: lib/pages/info/info_tabview.dart ================================================ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'package:kazumi/bean/card/comments_card.dart'; import 'package:kazumi/bean/card/character_card.dart'; import 'package:kazumi/bean/card/staff_card.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/modules/characters/character_item.dart'; import 'package:kazumi/modules/staff/staff_item.dart'; class InfoTabView extends StatefulWidget { const InfoTabView({ super.key, required this.commentsQueryTimeout, required this.charactersQueryTimeout, required this.staffQueryTimeout, required this.tabController, required this.loadMoreComments, required this.loadCharacters, required this.loadStaff, required this.bangumiItem, required this.commentsList, required this.characterList, required this.staffList, required this.isLoading, }); final bool commentsQueryTimeout; final bool charactersQueryTimeout; final bool staffQueryTimeout; final TabController tabController; final Future Function({int offset}) loadMoreComments; final Future Function() loadCharacters; final Future Function() loadStaff; final BangumiItem bangumiItem; final List commentsList; final List characterList; final List staffList; final bool isLoading; @override State createState() => _InfoTabViewState(); } class _InfoTabViewState extends State with SingleTickerProviderStateMixin { final maxWidth = 950.0; bool fullIntro = false; bool fullTag = false; Widget get infoBody { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('简介', style: TextStyle(fontSize: 18)), const SizedBox(height: 8), // https://stackoverflow.com/questions/54091055/flutter-how-to-get-the-number-of-text-lines // only show expand button when line > 7 LayoutBuilder(builder: (context, constraints) { final span = TextSpan(text: widget.bangumiItem.summary); final tp = TextPainter(text: span, textDirection: TextDirection.ltr); tp.layout(maxWidth: constraints.maxWidth); final numLines = tp.computeLineMetrics().length; if (numLines > 7) { return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox( // make intro expandable height: fullIntro ? null : 120, width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: SelectableText( widget.bangumiItem.summary, textAlign: TextAlign.start, scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), scrollPhysics: NeverScrollableScrollPhysics(), selectionHeightStyle: ui.BoxHeightStyle.max, ), ), TextButton( onPressed: () { setState(() { fullIntro = !fullIntro; }); }, child: Text(fullIntro ? '加载更少' : '加载更多'), ), ], ); } else { return SelectableText( widget.bangumiItem.summary, textAlign: TextAlign.start, scrollPhysics: NeverScrollableScrollPhysics(), selectionHeightStyle: ui.BoxHeightStyle.max, ); } }), const SizedBox(height: 16), Text('标签', style: TextStyle(fontSize: 18)), const SizedBox(height: 8), Wrap( spacing: 8.0, runSpacing: Utils.isDesktop() ? 8 : 0, children: List.generate( fullTag || widget.bangumiItem.tags.length < 13 ? widget.bangumiItem.tags.length : 13, (int index) { if (!fullTag && index == 12) { // make tag expandable return ActionChip( label: Text( '更多 +', style: TextStyle( color: Theme.of(context).colorScheme.primary), ), onPressed: () { setState(() { fullTag = !fullTag; }); }, ); } return ActionChip( label: Row( mainAxisSize: MainAxisSize.min, children: [ Text('${widget.bangumiItem.tags[index].name} '), Text( '${widget.bangumiItem.tags[index].count}', style: TextStyle( color: Theme.of(context).colorScheme.primary), ), ], ), onPressed: () { Modular.to.pushNamed( '/search/${widget.bangumiItem.tags[index].name}'); }, ); }).toList(), ) ], ), ), ), ); } /// Bone for Skeleton Loader Widget get infoBodyBone { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Skeletonizer.zone(child: Bone.text(fontSize: 18, width: 50)), const SizedBox(height: 8), Skeletonizer.zone(child: Bone.multiText(lines: 7)), const SizedBox(height: 16), Skeletonizer.zone(child: Bone.text(fontSize: 18, width: 50)), const SizedBox(height: 8), if (widget.isLoading) Skeletonizer.zone( child: Wrap( spacing: 8.0, runSpacing: 8.0, children: List.generate( 4, (_) => Bone.button(uniRadius: 8, height: 32)), ), ), ], ), ), ), ); } Widget get commentsListBody { return Builder( builder: (BuildContext context) { return NotificationListener( onNotification: (scrollEnd) { final metrics = scrollEnd.metrics; if (metrics.pixels >= metrics.maxScrollExtent - 200) { widget.loadMoreComments(offset: widget.commentsList.length); } return true; }, child: CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), key: PageStorageKey('吐槽'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverLayoutBuilder(builder: (context, _) { if (widget.commentsList.isNotEmpty) { return SliverList.separated( addAutomaticKeepAlives: false, itemCount: widget.commentsList.length, itemBuilder: (context, index) { return SafeArea( top: false, bottom: false, child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: CommentsCard( commentItem: widget.commentsList[index], ), ), ), ), ); }, separatorBuilder: (BuildContext context, int index) { return SafeArea( top: false, bottom: false, child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: Divider( thickness: 0.5, indent: 10, endIndent: 10), ), ), ), ); }, ); } if (widget.commentsQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( errMsg: '获取失败,请重试', actions: [ GeneralErrorButton( onPressed: () { widget.loadMoreComments( offset: widget.commentsList.length); }, text: '重试', ), ], ), ); } return SliverList.builder( itemCount: 4, itemBuilder: (context, _) { return SafeArea( top: false, bottom: false, child: Center( child: Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: CommentsCard.bone(), ), ), ), ); }, ); }) ], ), ); }, ); } Widget get staffListBody { return Builder( builder: (BuildContext context) { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), key: PageStorageKey('制作人员'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverLayoutBuilder(builder: (context, _) { if (widget.staffList.isNotEmpty) { return SliverList.builder( itemCount: widget.staffList.length, itemBuilder: (context, index) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: StaffCard( staffFullItem: widget.staffList[index], ), ), ), ); }, ); } if (widget.staffQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( errMsg: '获取失败,请重试', actions: [ GeneralErrorButton( onPressed: () { widget.loadStaff(); }, text: '重试', ), ], ), ); } return SliverList.builder( itemCount: 8, itemBuilder: (context, _) { return Align( alignment: Alignment.topCenter, child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: Skeletonizer.zone( child: ListTile( leading: Bone.circle(size: 36), title: Bone.text(width: 100), subtitle: Bone.text(width: 80), ), ), ), ); }, ); }), ], ); }, ); } Widget get charactersListBody { return Builder( builder: (BuildContext context) { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), key: PageStorageKey('角色'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverLayoutBuilder(builder: (context, _) { if (widget.characterList.isNotEmpty) { return SliverList.builder( itemCount: widget.characterList.length, itemBuilder: (context, index) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: CharacterCard( characterItem: widget.characterList[index], ), ), ), ); }, ); } if (widget.charactersQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( errMsg: '获取失败,请重试', actions: [ GeneralErrorButton( onPressed: () { widget.loadCharacters(); }, text: '重试', ), ], ), ); } return SliverList.builder( itemCount: 4, itemBuilder: (context, _) { return Align( alignment: Alignment.topCenter, child: SizedBox( width: MediaQuery.sizeOf(context).width > maxWidth ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: Skeletonizer.zone( child: ListTile( leading: Bone.circle(size: 36), title: Bone.text(width: 100), subtitle: Bone.text(width: 80), ), ), ), ); }, ); }), ], ); }, ); } @override Widget build(BuildContext context) { return TabBarView( controller: widget.tabController, children: [ Builder( // This Builder is needed to provide a BuildContext that is // "inside" the NestedScrollView, so that // sliverOverlapAbsorberHandleFor() can find the // NestedScrollView. builder: (BuildContext context) { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), // The PageStorageKey should be unique to this ScrollView; // it allows the list to remember its scroll position when // the tab view is not on the screen. key: PageStorageKey('概览'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverToBoxAdapter( child: SafeArea( top: false, bottom: false, child: widget.isLoading ? infoBodyBone : infoBody, ), ), ], ); }, ), commentsListBody, charactersListBody, Builder( builder: (BuildContext context) { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), key: PageStorageKey('评论'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), // TODO: 评论区 SliverFillRemaining( child: Center(child: Text('施工中')), ), ], ); }, ), staffListBody, ], ); } } ================================================ FILE: lib/pages/info/source_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/info/info_controller.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:kazumi/request/query_manager.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'dart:async'; import 'dart:convert'; import 'package:kazumi/providers/captcha/captcha_provider.dart'; import 'package:kazumi/plugins/anti_crawler_config.dart'; class SourceSheet extends StatefulWidget { const SourceSheet({ super.key, required this.tabController, required this.infoController, }); final TabController tabController; final InfoController infoController; @override State createState() => _SourceSheetState(); } class _SourceSheetState extends State with SingleTickerProviderStateMixin { final VideoPageController videoPageController = Modular.get(); final CollectController collectController = Modular.get(); final PluginsController pluginsController = Modular.get(); late String keyword; /// Concurrent query manager QueryManager? queryManager; /// Captcha solving provider (created on demand) CaptchaProvider? _captchaProvider; /// Timeout timer waiting for captcha verification result Timer? _captchaVerifyTimer; @override void initState() { keyword = widget.infoController.bangumiItem.nameCn == '' ? widget.infoController.bangumiItem.name : widget.infoController.bangumiItem.nameCn; queryManager = QueryManager(infoController: widget.infoController); queryManager?.queryAllSource(keyword); super.initState(); } @override void dispose() { queryManager?.cancel(); queryManager = null; _captchaProvider?.dispose(); _captchaProvider = null; _captchaVerifyTimer?.cancel(); _captchaVerifyTimer = null; super.dispose(); } /// 根据插件的验证类型分发到对应的验证对话框 void showAntiCrawlerDialog(Plugin plugin) { switch (plugin.antiCrawlerConfig.captchaType) { case CaptchaType.autoClickButton: showButtonClickDialog(plugin); break; default: showCaptchaDialog(plugin); } } void showCaptchaDialog(Plugin plugin) { final captchaImageNotifier = ValueNotifier(null); final submittingNotifier = ValueNotifier(false); final TextEditingController codeController = TextEditingController(); /// flag whether verification has passed, used to distinguish normal dismissal from cancellation in onDismiss bool verified = false; _captchaProvider?.dispose(); _captchaProvider = CaptchaProvider(); final searchUrl = plugin.searchURL.replaceAll('@keyword', keyword); _captchaProvider!.loadForCaptcha( searchUrl, plugin.antiCrawlerConfig.captchaImage, inputXpath: plugin.antiCrawlerConfig.captchaInput, ); final imageSub = _captchaProvider!.onCaptchaImageUrl.listen((url) { if (url != null) captchaImageNotifier.value = url; }); Future doSubmit() async { if (submittingNotifier.value) return; if (codeController.text.trim().isEmpty) { KazumiDialog.showToast(message: '请输入验证码'); return; } submittingNotifier.value = true; await _captchaProvider?.submitCaptcha( captchaCode: codeController.text.trim(), inputXpath: plugin.antiCrawlerConfig.captchaInput, buttonXpath: plugin.antiCrawlerConfig.captchaButton, pluginName: plugin.name, onVerified: () { _captchaVerifyTimer?.cancel(); _captchaVerifyTimer = null; verified = true; KazumiDialog.dismiss(); // show a 3s countdown progress dialog before re-querying, // to avoid triggering rate limits immediately after verification. KazumiDialog.showTimedSuccessDialog( title: '验证成功', message: '正在重新检索,请稍候…', onComplete: () => queryManager?.querySource(keyword, plugin.name), ); }, ); // submitCaptcha completes after the JS button click is fired. // Start the 8-second timeout only NOW, waiting for the webview to // detect the captcha disappearing and call onVerified. if (!verified) { _captchaVerifyTimer?.cancel(); _captchaVerifyTimer = Timer(const Duration(seconds: 8), () { if (!verified) { KazumiDialog.dismiss(); } }); } } KazumiDialog.show( onDismiss: () async { _captchaVerifyTimer?.cancel(); _captchaVerifyTimer = null; // Cancel the image subscription before disposing the notifier to // prevent late stream events writing to an already-disposed notifier. imageSub.cancel(); codeController.dispose(); captchaImageNotifier.dispose(); submittingNotifier.dispose(); // Capture the current provider instance locally NOW, before any await. // Without this, an async gap could allow _captchaProvider to be // replaced (or nulled by _SourceSheetState.dispose()), causing the // closure to dispose the wrong/already-disposed instance. final provider = _captchaProvider; _captchaProvider = null; if (!verified) { await provider?.saveAndUnload(plugin.name); provider?.dispose(); queryManager?.querySource(keyword, plugin.name); } else { provider?.dispose(); } }, builder: (context) { return Dialog( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(24), child: SizedBox( width: 400, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '验证码验证', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), Text( '${plugin.name} 需要验证码验证', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 20), ValueListenableBuilder( valueListenable: captchaImageNotifier, builder: (context, imageUrl, _) { if (imageUrl == null) { return const Column( children: [ CircularProgressIndicator(), SizedBox(height: 12), Text('正在加载验证码图片...'), ], ); } return ValueListenableBuilder( valueListenable: submittingNotifier, builder: (context, isSubmitting, _) { return Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.memory( base64Decode(imageUrl.split(',').last), height: 80, fit: BoxFit.contain, errorBuilder: (context, error, _) => const Text('图片解码失败'), ), ), const SizedBox(height: 16), TextField( controller: codeController, autofocus: true, enabled: !isSubmitting, decoration: const InputDecoration( labelText: '请输入验证码', border: OutlineInputBorder(), ), onSubmitted: isSubmitting ? null : (_) => doSubmit(), ), ], ); }, ); }, ), const SizedBox(height: 20), ListenableBuilder( listenable: Listenable.merge( [captchaImageNotifier, submittingNotifier]), builder: (context, _) { final isImageLoading = captchaImageNotifier.value == null; final isSubmitting = submittingNotifier.value; final isDisabled = isImageLoading || isSubmitting; return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), ), const SizedBox(width: 8), FilledButton( onPressed: isDisabled ? null : doSubmit, child: isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Text('提交'), ), ], ); }, ), ], ), ), ), ); }, ); } void showButtonClickDialog(Plugin plugin) { /// flag whether onVerified was fired by the auto-click flow (cookies already saved + page unloaded) bool autoVerified = false; _captchaProvider?.dispose(); _captchaProvider = CaptchaProvider(); final searchUrl = plugin.searchURL.replaceAll('@keyword', keyword); void onVerified() { if (autoVerified) return; autoVerified = true; KazumiDialog.dismiss(); // show a 3s countdown progress dialog before re-querying KazumiDialog.showTimedSuccessDialog( title: '验证成功', message: '正在重新检索,请稍候…', onComplete: () => queryManager?.querySource(keyword, plugin.name), ); } _captchaProvider!.loadForButtonClick( url: searchUrl, buttonXpath: plugin.antiCrawlerConfig.captchaButton, pluginName: plugin.name, onVerified: onVerified, ); KazumiDialog.show( onDismiss: () async { // Capture the current provider instance locally before any await. final provider = _captchaProvider; _captchaProvider = null; if (autoVerified) { // auto-verify already saved cookies and unloaded the page provider?.dispose(); } else { // save whatever cookies are present and unload the page await provider?.saveAndUnload(plugin.name); provider?.dispose(); queryManager?.querySource(keyword, plugin.name); } }, builder: (context) => Dialog( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(24), child: SizedBox( width: 400, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '自动验证中', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), Text( '${plugin.name} 正在自动完成验证,请稍候', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 24), const CircularProgressIndicator(), const SizedBox(height: 12), Text( '已检测到验证按钮并模拟点击,等待验证通过…', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 20), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), ), ), ], ), ), ), ), ); } Widget buildPluginView(Plugin plugin, List cardList) { final status = widget.infoController.pluginSearchStatus[plugin.name]; if (status == 'pending') { return const Center(child: CircularProgressIndicator()); } if (status == 'captcha') { return GeneralErrorWidget( errMsg: '${plugin.name} 需要验证码验证', actions: [ GeneralErrorButton( onPressed: () => showAntiCrawlerDialog(plugin), text: '进行验证', ), GeneralErrorButton( onPressed: () => queryManager?.querySource(keyword, plugin.name), text: '重试', ), ], ); } if (status == 'noResult') { return GeneralErrorWidget( errMsg: '${plugin.name} 无结果 使用别名或左右滑动以切换到其他视频来源', actions: [ GeneralErrorButton( onPressed: () => showAliasSearchDialog(plugin.name), text: '别名检索', ), GeneralErrorButton( onPressed: () => showCustomSearchDialog(plugin.name), text: '手动检索', ), ], ); } if (status == 'error') { return GeneralErrorWidget( errMsg: '${plugin.name} 检索失败 重试或左右滑动以切换到其他视频来源', actions: [ GeneralErrorButton( onPressed: () => queryManager?.querySource(keyword, plugin.name), text: '重试', ), ], ); } return ListView(children: cardList); } void showAliasSearchDialog(String pluginName) { if (widget.infoController.bangumiItem.alias.isEmpty) { KazumiDialog.showToast(message: '无可用别名,试试手动检索'); return; } final aliasNotifier = ValueNotifier>(widget.infoController.bangumiItem.alias); KazumiDialog.show(builder: (context) { return Dialog( clipBehavior: Clip.antiAlias, child: SizedBox( width: 560, child: ValueListenableBuilder>( valueListenable: aliasNotifier, builder: (context, aliasList, child) { return ListView( shrinkWrap: true, children: aliasList.asMap().entries.map((entry) { final index = entry.key; final alias = entry.value; return ListTile( title: Text(alias), trailing: IconButton( onPressed: () { KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('删除确认'), content: const Text('删除后无法恢复,确认要永久删除这个别名吗?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( '取消', style: TextStyle( color: Theme.of(context) .colorScheme .outline), ), ), TextButton( onPressed: () { KazumiDialog.dismiss(); aliasList.removeAt(index); aliasNotifier.value = List.from(aliasList); collectController.updateLocalCollect( widget.infoController.bangumiItem); if (aliasList.isEmpty) { // pop whole dialog when empty Navigator.of(context).pop(); } }, child: const Text('确认'), ), ], ); }, ); }, icon: Icon(Icons.delete), ), onTap: () { KazumiDialog.dismiss(); queryManager?.querySource(alias, pluginName); }, ); }).toList(), ); }, ), ), ); }); } void showCustomSearchDialog(String pluginName) { KazumiDialog.show( builder: (context) { final TextEditingController textController = TextEditingController(); return AlertDialog( title: const Text('输入别名'), content: TextField( controller: textController, onSubmitted: (keyword) { if (textController.text != '') { widget.infoController.bangumiItem.alias .add(textController.text); KazumiDialog.dismiss(); queryManager?.querySource(textController.text, pluginName); } }, ), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { if (textController.text != '') { widget.infoController.bangumiItem.alias .add(textController.text); collectController .updateLocalCollect(widget.infoController.bangumiItem); KazumiDialog.dismiss(); queryManager?.querySource(textController.text, pluginName); } }, child: const Text( '确认', ), ), ], ); }, ); } @override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Scaffold( body: Column( children: [ Row( children: [ Expanded( child: TabBar( isScrollable: true, tabAlignment: TabAlignment.center, dividerHeight: 0, controller: widget.tabController, tabs: pluginsController.pluginList .map( (plugin) => Observer( builder: (context) { return Tab( child: Row( children: [ Text( plugin.name, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: Theme.of(context) .textTheme .titleMedium! .fontSize, color: Theme.of(context) .colorScheme .onSurface), ), const SizedBox(width: 5.0), Container( width: 8.0, height: 8.0, decoration: BoxDecoration( color: switch (widget.infoController .pluginSearchStatus[plugin.name]) { 'success' => Colors.green, 'noResult' => Colors.orange, 'captcha' => Colors.blue, 'error' => Colors.red, _ => Colors.grey, }, shape: BoxShape.circle, ), ), ], ), ); }, ), ) .toList(), ), ), IconButton( onPressed: () { int currentIndex = widget.tabController.index; launchUrl( Uri.parse(pluginsController .pluginList[currentIndex].searchURL .replaceFirst('@keyword', keyword)), mode: LaunchMode.externalApplication, ); }, icon: const Icon(Icons.open_in_browser_rounded), ), const SizedBox(width: 4), ], ), const Divider(height: 1), Expanded( child: Observer( builder: (context) => TabBarView( controller: widget.tabController, children: List.generate(pluginsController.pluginList.length, (pluginIndex) { var plugin = pluginsController.pluginList[pluginIndex]; var cardList = []; for (var searchResponse in widget.infoController.pluginSearchResponseList) { if (searchResponse.pluginName == plugin.name) { for (var searchItem in searchResponse.data) { cardList.add( Card( elevation: 0, margin: const EdgeInsets.only( left: 10, right: 10, top: 10), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () async { KazumiDialog.showLoading( msg: '获取中', barrierDismissible: Utils.isDesktop(), onDismiss: () { videoPageController.cancelQueryRoads(); }, ); videoPageController.bangumiItem = widget.infoController.bangumiItem; videoPageController.currentPlugin = plugin; videoPageController.title = searchItem.name; videoPageController.src = searchItem.src; try { await videoPageController.queryRoads( searchItem.src, plugin.name); KazumiDialog.dismiss(); Modular.to.pushNamed('/video/'); } catch (_) { KazumiLogger().w( "QueryManager: failed to query video playlist"); KazumiDialog.dismiss(); } }, child: Padding( padding: const EdgeInsets.all(20), child: Text(searchItem.name), ), ), ), ); } } } return buildPluginView(plugin, cardList); }), ), ), ) ], ), ), ); } } ================================================ FILE: lib/pages/init_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/my/my_controller.dart'; import 'package:kazumi/utils/webdav.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:provider/provider.dart'; import 'package:kazumi/bean/settings/theme_provider.dart'; import 'package:kazumi/shaders/shaders_controller.dart'; import 'package:kazumi/pages/download/download_controller.dart'; import 'package:kazumi/utils/background_download_service.dart'; class InitPage extends StatefulWidget { const InitPage({super.key}); @override State createState() => _InitPageState(); } class _InitPageState extends State { final PluginsController pluginsController = Modular.get(); final CollectController collectController = Modular.get(); final ShadersController shadersController = Modular.get(); final MyController myController = Modular.get(); final DownloadController downloadController = Modular.get(); Box setting = GStorage.setting; late final ThemeProvider themeProvider; @override void initState() { super.initState(); themeProvider = Provider.of(context, listen: false); _initializeApp(); } Future _initializeApp() async { _migrateStorage(); _loadShaders(); _loadDanmakuShield(); _webDavInit(); try { await downloadController.init(); _setupBackgroundDownloadNavigation(); } catch (e) { KazumiLogger().e('InitPage: downloadController.init() failed', error: e); } await _checkRunningOnX11(); await _pluginInit(); _startDefaultPage(); // delay to ensure that the default page is fully loaded await Future.delayed(const Duration(milliseconds: 500)); _update(); } void _setupBackgroundDownloadNavigation() { final backgroundService = BackgroundDownloadService(); backgroundService.onNavigateToDownloadRequested = () { Future.delayed(const Duration(milliseconds: 300), () { try { if (Modular.to.path.contains('/download')) return; Modular.to.pushNamed('/settings/download/'); } catch (e) { KazumiLogger() .w('InitPage: failed to navigate to download page', error: e); } }); }; backgroundService.onNotificationPermissionRequired = () async { final result = await KazumiDialog.show( clickMaskDismiss: false, builder: (context) { return AlertDialog( title: const Text('需要通知权限'), content: const Text( '开启通知权限后,可以在后台下载时显示进度,并防止系统终止下载任务。\n\n' '如果拒绝,下载功能仍可使用,但在后台时可能被系统中断。', ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(popWith: false), child: Text( '稍后再说', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () => KazumiDialog.dismiss(popWith: true), child: const Text('允许'), ), ], ); }, ); return result ?? false; }; } void _startDefaultPage() { final defaultStartupPage = setting.get( SettingBoxKey.defaultStartupPage, defaultValue: '/tab/popular/', ); // Workaround for dynamic_color. dynamic_color need PlatformChannel to get color, it takes time. // setDynamic here to avoid white screen flash when themeMode is dark. themeProvider.setDynamic( setting.get(SettingBoxKey.useDynamicColor, defaultValue: false)); Modular.to.navigate(defaultStartupPage); } // migrate collect from old version (favorites) Future _migrateStorage() async { await collectController.migrateCollect(); } Future _loadShaders() async { await shadersController.copyShadersToExternalDirectory(); } Future _loadDanmakuShield() async { myController.loadShieldList(); } Future _webDavInit() async { bool webDavEnable = await setting.get(SettingBoxKey.webDavEnable, defaultValue: false); if (webDavEnable) { var webDav = WebDav(); KazumiLogger().i('WebDav: Starting WebDav initialization'); try { await webDav.init(); try { await webDav.downloadAndPatchHistory(); KazumiLogger().i('WebDav: Completed syncing watch history'); } catch (e) { KazumiDialog.showToast(message: "同步观看记录失败 ${e.toString()}"); } } catch (e) { KazumiDialog.showToast(message: "初始化WebDav失败 ${e.toString()}"); } } } Future _checkRunningOnX11() async { if (!Platform.isLinux) { return; } bool isRunningOnX11 = await Utils.isRunningOnX11(); if (isRunningOnX11) { await KazumiDialog.show( clickMaskDismiss: false, builder: (context) { return PopScope( canPop: false, child: AlertDialog( title: const Text('X11环境检测'), content: const Text( '检测到您当前运行在X11环境下,Kazumi在X11环境下可能出现性能问题或界面异常,建议切换到Wayland以获得更好的体验。您是否希望在X11下继续使用Kazumi?'), actions: [ TextButton( onPressed: () { exit(0); }, child: Text( '退出', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: const Text('继续'), ), ], ), ); }, ); } } Future _pluginInit() async { String statementsText = ''; try { await pluginsController.init(); statementsText = await rootBundle.loadString("assets/statements/statements.txt"); _pluginUpdate(); } catch (_) {} if (pluginsController.pluginList.isEmpty) { await KazumiDialog.show( clickMaskDismiss: false, builder: (context) { return PopScope( canPop: false, child: AlertDialog( title: const Text('免责声明'), scrollable: true, content: Text(statementsText), actions: [ TextButton( onPressed: () { exit(0); }, child: Text( '退出', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { try { await pluginsController.copyPluginsToExternalDirectory(); } catch (_) {} KazumiDialog.dismiss(); if (!Platform.isAndroid) { return; } await _switchUpdateMirror(); }, child: const Text('已阅读并同意'), ), ], ), ); }, ); } } // The function is not completed yet // We simply disable update when the user is using F-Droid mirror // We are trying to meet F-Droid requirement to submit the app // After the app is submitted, we will complete the function Future _switchUpdateMirror() async { await KazumiDialog.show( clickMaskDismiss: false, builder: (context) { return PopScope( canPop: false, child: AlertDialog( title: const Text('更新镜像'), content: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text( '您希望从哪里获取应用更新?', textAlign: TextAlign.left, ), ), Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text( 'Github镜像为大多数情况下的最佳选择。如果您使用F-Droid应用商店, 请选择F-Droid镜像。', textAlign: TextAlign.left, ), ), ], ), actions: [ TextButton( onPressed: () { setting.put(SettingBoxKey.autoUpdate, true); KazumiDialog.dismiss(); }, child: const Text( 'Github', ), ), TextButton( onPressed: () { setting.put(SettingBoxKey.autoUpdate, false); KazumiDialog.dismiss(); }, child: Text( 'F-Droid', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), ], ), ); }, ); } Future _update() async { bool autoUpdate = await setting.get(SettingBoxKey.autoUpdate, defaultValue: true); if (autoUpdate) { Modular.get().checkUpdate(type: 'auto'); } } Future _pluginUpdate() async { await pluginsController.queryPluginHTTPList(); int count = 0; for (var plugin in pluginsController.pluginList) { if (pluginsController.pluginUpdateStatus(plugin) == 'updatable') { count++; } } if (count != 0) { KazumiDialog.showToast(message: '检测到 $count 条规则可以更新'); } } @override Widget build(BuildContext context) { return const LoadingWidget(); } } class LoadingWidget extends StatelessWidget { const LoadingWidget({super.key}); @override Widget build(BuildContext context) { return Scaffold(body: Container()); } } ================================================ FILE: lib/pages/logs/logs_page.dart ================================================ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; class LogsPage extends StatefulWidget { const LogsPage({super.key}); @override State createState() => _LogsPageState(); } class _LogsPageState extends State { final List _logLines = []; final ScrollController _scrollController = ScrollController(); bool _isLoading = true; bool _hasError = false; String _fullContent = ''; static const int _initialLoadCount = 50; static const int _loadMoreCount = 100; int _displayedLines = 0; List _allLines = []; @override void initState() { super.initState(); _loadLogs(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (!mounted || _displayedLines >= _allLines.length) { return; } final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; final threshold = maxScroll * 0.8; if (currentScroll >= threshold) { _loadMoreLines(); } } Future _loadLogs() async { if (!mounted) return; try { final file = await _getLogsFile(); if (!mounted) return; if (await file.exists()) { final content = await file.readAsString(); if (!mounted) return; _allLines = content.split('\n'); _fullContent = content; final initialCount = _allLines.length < _initialLoadCount ? _allLines.length : _initialLoadCount; if (!mounted) return; setState(() { _logLines.clear(); _logLines.addAll(_allLines.take(initialCount)); _displayedLines = initialCount; _isLoading = false; }); } else { if (!mounted) return; setState(() { _isLoading = false; }); } } catch (e) { if (!mounted) return; setState(() { _hasError = true; _isLoading = false; }); } } void _loadMoreLines() { if (_displayedLines >= _allLines.length) { return; } // 使用 Future.microtask 避免在构建过程中调用 setState Future.microtask(() { if (!mounted) return; final remainingLines = _allLines.length - _displayedLines; final linesToLoad = remainingLines < _loadMoreCount ? remainingLines : _loadMoreCount; final newLines = _allLines.skip(_displayedLines).take(linesToLoad); if (!mounted) return; setState(() { _logLines.addAll(newLines); _displayedLines += linesToLoad; }); }); } Future _getLogsFile() async { final directory = await getApplicationSupportDirectory(); final path = directory.path; return File('$path/logs/kazumi_logs.log'); } Future _clearLogs() async { try { final file = await _getLogsFile(); await file.writeAsString(''); if (!mounted) return; setState(() { _logLines.clear(); _allLines.clear(); _fullContent = ''; _displayedLines = 0; }); } catch (e) { if (!mounted) return; KazumiDialog.showToast(message: '清空失败: $e'); } } Future _copyLogs() async { try { await Clipboard.setData(ClipboardData(text: _fullContent)); if (!mounted) return; KazumiDialog.showToast(message: '已复制到剪贴板'); } catch (e) { if (!mounted) return; KazumiDialog.showToast(message: '复制失败: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar( title: Text('日志'), ), body: buildBody, floatingActionButton: buildFloatingButtons, ); } Widget get buildBody { if (_isLoading) { return const Center( child: CircularProgressIndicator(), ); } if (_hasError) { return const Center( child: Text('加载日志失败'), ); } if (_logLines.isEmpty) { return const Center( child: Text('没有数据'), ); } return SelectionArea( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: MediaQuery.of(context).size.width.clamp(600, double.infinity), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16.0), shrinkWrap: false, itemCount: _logLines.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Text( _logLines[index], softWrap: false, overflow: TextOverflow.clip, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, ), ), ); }, ), ), ), ); } Widget get buildFloatingButtons { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( heroTag: null, onPressed: _clearLogs, tooltip: '清空日志', child: const Icon(Icons.clear_all), ), const SizedBox(width: 15), FloatingActionButton( heroTag: null, onPressed: _copyLogs, tooltip: '复制日志', child: const Icon(Icons.copy), ), ], ); } } ================================================ FILE: lib/pages/menu/menu.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/pages/router.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:provider/provider.dart'; class ScaffoldMenu extends StatefulWidget { const ScaffoldMenu({super.key}); @override State createState() => _ScaffoldMenu(); } class NavigationBarState extends ChangeNotifier { late int _selectedIndex = getDefaultSelectedIndex(); bool _isHide = false; bool _isBottom = false; int get selectedIndex => _selectedIndex; bool get isHide => _isHide; bool get isBottom => _isBottom; int getDefaultSelectedIndex() { final defaultPage = GStorage.setting .get(SettingBoxKey.defaultStartupPage, defaultValue: "/tab/popular/"); switch (defaultPage) { case "/tab/popular/": return 0; case "/tab/timeline/": return 1; case "/tab/collect/": return 2; case "/tab/my/": return 3; default: return 0; } } void updateSelectedIndex(int pageIndex) { _selectedIndex = pageIndex; notifyListeners(); } void hideNavigate() { _isHide = true; notifyListeners(); } void showNavigate() { _isHide = false; notifyListeners(); } } class _ScaffoldMenu extends State { final PageController _page = PageController(); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => NavigationBarState(), child: Consumer(builder: (context, state, _) { return OrientationBuilder(builder: (context, orientation) { state._isBottom = orientation == Orientation.portrait; return orientation != Orientation.portrait ? sideMenuWidget(context, state) : bottomMenuWidget(context, state); }); })); } Widget bottomMenuWidget(BuildContext context, NavigationBarState state) { return Scaffold( body: Container( color: Theme.of(context).colorScheme.primaryContainer, child: PageView.builder( physics: const NeverScrollableScrollPhysics(), controller: _page, itemCount: menu.size, itemBuilder: (_, __) => const RouterOutlet(), ), ), bottomNavigationBar: state.isHide ? const SizedBox(height: 0) : NavigationBar( destinations: const [ NavigationDestination( selectedIcon: Icon(Icons.home), icon: Icon(Icons.home_outlined), label: '推荐', ), NavigationDestination( selectedIcon: Icon(Icons.timeline), icon: Icon(Icons.timeline_outlined), label: '时间表', ), NavigationDestination( selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_outlined), label: '追番', ), NavigationDestination( selectedIcon: Icon(Icons.settings), icon: Icon(Icons.settings), label: '我的', ), ], selectedIndex: state.selectedIndex, onDestinationSelected: (int index) { state.updateSelectedIndex(index); Modular.to.navigate("/tab${menu.getPath(index)}/"); }, )); } Widget sideMenuWidget(BuildContext context, NavigationBarState state) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, body: Row( children: [ EmbeddedNativeControlArea( child: Visibility( visible: !state.isHide, child: NavigationRail( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, groupAlignment: 1.0, leading: FloatingActionButton( elevation: 0, heroTag: null, onPressed: () { Modular.to.pushNamed('/search/'); }, child: const Icon(Icons.search), ), labelType: NavigationRailLabelType.selected, destinations: const [ NavigationRailDestination( selectedIcon: Icon(Icons.home), icon: Icon(Icons.home_outlined), label: Text('推荐'), ), NavigationRailDestination( selectedIcon: Icon(Icons.timeline), icon: Icon(Icons.timeline_outlined), label: Text('时间表'), ), NavigationRailDestination( selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_border), label: Text('追番'), ), NavigationRailDestination( selectedIcon: Icon(Icons.settings), icon: Icon(Icons.settings_outlined), label: Text('我的'), ), ], selectedIndex: state.selectedIndex, onDestinationSelected: (int index) { state.updateSelectedIndex(index); Modular.to.navigate("/tab${menu.getPath(index)}/"); }, ), ), ), Expanded( child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), bottomLeft: Radius.circular(16.0), ), ), child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), bottomLeft: Radius.circular(16.0), ), child: PageView.builder( physics: const NeverScrollableScrollPhysics(), itemCount: menu.size, itemBuilder: (_, __) => const RouterOutlet(), ), ), ), ), ], ), ); } } ================================================ FILE: lib/pages/my/my_controller.dart ================================================ import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:mobx/mobx.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/auto_updater.dart'; part 'my_controller.g.dart'; class MyController = _MyController with _$MyController; abstract class _MyController with Store { Box setting = GStorage.setting; @observable ObservableList shieldList = ObservableList.of([]); bool isDanmakuBlocked(String? danmaku) { if (danmaku == null || danmaku.isEmpty) return false; for (String item in shieldList) { if (item.isEmpty) continue; if (item.startsWith('/') && item.endsWith('/')) { if (item.length <= 2) continue; String pattern = item.substring(1, item.length - 1); try { if (RegExp(pattern).hasMatch(danmaku)) return true; } catch (_) { KazumiLogger().e('Danmaku: invalid danmaku shield regex pattern: $pattern'); continue; } } else { if (danmaku.contains(item)) return true; } } return false; } void loadShieldList() { shieldList.clear(); shieldList.addAll(GStorage.shieldList.values.toList()); } void addShieldList(String item) { if (item.isEmpty) { KazumiDialog.showToast(message: '请输入关键词'); return; } if (item.length > 64) { KazumiDialog.showToast(message: '关键词过长'); return; } if (shieldList.contains(item)) { KazumiDialog.showToast(message: '已存在该关键词'); return; } shieldList.add(item); GStorage.shieldList.put(item, item); GStorage.shieldList.flush(); } void removeShieldList(String item) { shieldList.remove(item); GStorage.shieldList.delete(item); GStorage.shieldList.flush(); } Future checkUpdate({String type = 'manual'}) async { try { final autoUpdater = AutoUpdater(); if (type == 'manual') { await autoUpdater.manualCheckForUpdates(); } else { await autoUpdater.autoCheckForUpdates(); } return true; } catch (err) { KazumiLogger().e('Update: check update failed', error: err); if (type == 'manual') { KazumiDialog.showToast(message: '检查更新失败,请稍后重试'); } return false; } } } ================================================ FILE: lib/pages/my/my_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'my_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$MyController on _MyController, Store { late final _$shieldListAtom = Atom(name: '_MyController.shieldList', context: context); @override ObservableList get shieldList { _$shieldListAtom.reportRead(); return super.shieldList; } @override set shieldList(ObservableList value) { _$shieldListAtom.reportWrite(value, super.shieldList, () { super.shieldList = value; }); } @override String toString() { return ''' shieldList: ${shieldList} '''; } } ================================================ FILE: lib/pages/my/my_module.dart ================================================ import 'package:kazumi/pages/my/my_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class MyModule extends Module { @override void routes(r) { r.child("/", child: (_) => const MyPage()); } } ================================================ FILE: lib/pages/my/my_page.dart ================================================ import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/pages/menu/menu.dart'; import 'package:provider/provider.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; class MyPage extends StatefulWidget { const MyPage({super.key}); @override State createState() => _MyPageState(); } class _MyPageState extends State { late NavigationBarState navigationBarState; void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } navigationBarState.updateSelectedIndex(0); Modular.to.navigate('/tab/popular/'); } @override void initState() { super.initState(); navigationBarState = Provider.of(context, listen: false); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('我的'), needTopOffset: false), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('播放历史与视频源', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/history/'); }, leading: const Icon(Icons.history_rounded), title: Text('历史记录', style: TextStyle(fontFamily: fontFamily)), description: Text('查看播放历史记录', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/download/'); }, leading: const Icon(Icons.download_rounded), title: Text('下载管理', style: TextStyle(fontFamily: fontFamily)), description: Text('查看和管理离线下载', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/download-settings'); }, leading: const Icon(Icons.settings_rounded), title: Text('下载设置', style: TextStyle(fontFamily: fontFamily)), description: Text('配置下载并发数等参数', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/plugin/'); }, leading: const Icon(Icons.extension), title: Text('规则管理', style: TextStyle(fontFamily: fontFamily)), description: Text('管理番剧资源规则', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('播放器设置', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/player'); }, leading: const Icon(Icons.display_settings_rounded), title: Text('播放设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置播放器相关参数', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/danmaku/'); }, leading: const Icon(Icons.subtitles_rounded), title: Text('弹幕设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置弹幕相关参数', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/keyboard'); }, leading: const Icon(Icons.keyboard_rounded), title: Text('操作设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置播放器按键映射', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/proxy'); }, leading: const Icon(Icons.vpn_key_rounded), title: Text('代理设置', style: TextStyle(fontFamily: fontFamily)), description: Text('配置HTTP代理', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('应用与外观', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/theme'); }, leading: const Icon(Icons.palette_rounded), title: Text('外观设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置应用主题和刷新率', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/interface'); }, leading: const Icon(Icons.pages_rounded), title: Text('界面设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置应用界面样式', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/webdav/'); }, leading: const Icon(Icons.cloud), title: Text('同步设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置同步参数', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('其他', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/about/'); }, leading: const Icon(Icons.info_outline_rounded), title: Text('关于', style: TextStyle(fontFamily: fontFamily)), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/player/episode_comments_sheet.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/card/episode_comments_card.dart'; import 'package:kazumi/pages/video/video_controller.dart'; class EpisodeInfo extends InheritedWidget { /// This widget receives changes of episode and notify it's child, /// trigger [didChangeDependencies] of it's child. const EpisodeInfo({super.key, required this.episode, required super.child}); final int episode; @override bool updateShouldNotify(covariant InheritedWidget oldWidget) => true; static EpisodeInfo? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } } class EpisodeCommentsSheet extends StatefulWidget { const EpisodeCommentsSheet({super.key}); @override State createState() => _EpisodeCommentsSheetState(); } class _EpisodeCommentsSheetState extends State { final VideoPageController videoPageController = Modular.get(); bool commentsQueryTimeout = false; final GlobalKey _refreshIndicatorKey = GlobalKey(); /// episode input by [showEpisodeSelection] int ep = 0; @override void initState() { super.initState(); } Future loadComments(int episode) async { commentsQueryTimeout = false; await videoPageController .queryBangumiEpisodeCommentsByID( videoPageController.bangumiItem.id, episode) .then((_) { if (videoPageController.episodeCommentsList.isEmpty && mounted) { setState(() { commentsQueryTimeout = true; }); } }); if (mounted) { setState(() {}); } } void toggleSortOrder() { videoPageController.toggleSortOrder(); } @override void didChangeDependencies() { ep = 0; // wait until currentState is not null WidgetsBinding.instance.addPostFrameCallback((_) { if (videoPageController.episodeCommentsList.isEmpty) { // trigger RefreshIndicator onRefresh and show animation _refreshIndicatorKey.currentState?.show(); } }); super.didChangeDependencies(); } @override void dispose() { super.dispose(); } Widget get episodeCommentsBody { return CustomScrollView( scrollBehavior: const ScrollBehavior().copyWith( // Scrollbars' movement is not linear so hide it. scrollbars: false, // Enable mouse drag to refresh dragDevices: { PointerDeviceKind.mouse, PointerDeviceKind.touch, PointerDeviceKind.trackpad }, ), slivers: [ SliverPadding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 4), sliver: Observer(builder: (context) { if (commentsQueryTimeout) { return const SliverFillRemaining( child: Center( child: Text('空空如也'), ), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { // Fix scroll issue caused by height change of network images // by keeping loaded cards alive. return KeepAlive( keepAlive: true, child: IndexedSemantics( index: index, child: SelectionArea( child: EpisodeCommentsCard( commentItem: videoPageController.episodeCommentsList[index], ), ), ), ); }, childCount: videoPageController.episodeCommentsList.length, addAutomaticKeepAlives: false, addRepaintBoundaries: false, addSemanticIndexes: false, ), ); }), ), ], ); } Widget get commentsInfo { return Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text(' 本集标题 '), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.name}', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline)), Text( (videoPageController.episodeInfo.nameCn != '') ? '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.nameCn}' : '${videoPageController.episodeInfo.readType()}.${videoPageController.episodeInfo.episode} ${videoPageController.episodeInfo.name}', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline)), ], ), ), const SizedBox(width: 10), SizedBox( height: 34, child: TextButton( style: ButtonStyle( padding: WidgetStateProperty.all( const EdgeInsets.only(left: 4.0, right: 4.0)), ), onPressed: () { showEpisodeSelection(); }, child: const Text( '手动切换', style: TextStyle(fontSize: 13), ), ), ), SizedBox( height: 34, child: TextButton( style: ButtonStyle( padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 4.0)), ), onPressed: toggleSortOrder, child: Observer(builder: (context) { return Text( videoPageController.isCommentsAscending ? '倒序' : '正序', style: const TextStyle(fontSize: 13), ); }), ), ), ], ), ); } // 选择要查看评论的集数 void showEpisodeSelection() { final TextEditingController textController = TextEditingController(); KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('输入集数'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( inputFormatters: [ FilteringTextInputFormatter.digitsOnly ], controller: textController, ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { if (textController.text.isEmpty) { KazumiDialog.showToast(message: '请输入集数'); return; } ep = int.tryParse(textController.text) ?? 0; if (ep == 0) { return; } _refreshIndicatorKey.currentState?.show(); KazumiDialog.dismiss(); }, child: const Text('刷新'), ), ], ); }, ); } @override Widget build(BuildContext context) { final int episode = EpisodeInfo.of(context)!.episode; return Scaffold( body: RefreshIndicator( key: _refreshIndicatorKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [commentsInfo, Expanded(child: episodeCommentsBody)], ), onRefresh: () async { await loadComments(ep == 0 ? episode : ep); }, ), ); } } ================================================ FILE: lib/pages/player/player_controller.dart ================================================ import 'dart:io'; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:kazumi/modules/danmaku/danmaku_module.dart'; import 'package:mobx/mobx.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:kazumi/request/damaku.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/shaders/shaders_controller.dart'; import 'package:kazumi/utils/syncplay.dart'; import 'package:kazumi/utils/syncplay_endpoint.dart'; import 'package:kazumi/utils/external_player.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:kazumi/pages/download/download_controller.dart'; part 'player_controller.g.dart'; class PlaybackInitParams { final String videoUrl; final int offset; final bool isLocalPlayback; final int bangumiId; final String pluginName; final int episode; final Map httpHeaders; final bool adBlockerEnabled; final String episodeTitle; final String referer; final int currentRoad; const PlaybackInitParams({ required this.videoUrl, required this.offset, required this.isLocalPlayback, required this.bangumiId, required this.pluginName, required this.episode, required this.httpHeaders, required this.adBlockerEnabled, required this.episodeTitle, required this.referer, required this.currentRoad, }); } enum DanmakuDestination { chatRoom, remoteDanmaku, } class SyncPlayChatMessage { final String username; final String message; final bool fromRemote; final DateTime time; SyncPlayChatMessage({ required this.username, required this.message, this.fromRemote = true, DateTime? time, }) : time = time ?? DateTime.now(); } class PlayerController = _PlayerController with _$PlayerController; abstract class _PlayerController with Store { final ShadersController shadersController = Modular.get(); late int bangumiId; late int currentEpisode; late int currentRoad; late String referer; // 弹幕控制 late DanmakuController danmakuController; @observable Map> danDanmakus = {}; @observable bool danmakuOn = false; @observable bool danmakuLoading = false; DanmakuDestination danmakuDestination = DanmakuDestination.remoteDanmaku; final StreamController syncPlayChatStreamController = StreamController.broadcast(); Stream get syncPlayChatStream => syncPlayChatStreamController.stream; // 一起看控制器 SyncplayClient? syncplayController; @observable String syncplayRoom = ''; @observable int syncplayClientRtt = 0; /// 视频比例类型 /// 1. AUTO /// 2. COVER /// 3. FILL @observable int aspectRatioType = 1; /// 视频超分 /// 1. OFF /// 2. Anime4K Efficiency /// 3. Anime4K Quality @observable int superResolutionType = 1; // 视频音量/亮度 @observable double volume = -1; @observable double brightness = 0; // 播放器界面控制 @observable bool lockPanel = false; @observable bool showVideoController = true; @observable bool showSeekTime = false; @observable bool showBrightness = false; @observable bool showVolume = false; @observable bool showPlaySpeed = false; @observable bool brightnessSeeking = false; @observable bool volumeSeeking = false; @observable bool canHidePlayerPanel = true; // 视频地址 String videoUrl = ''; // DanDanPlay 弹幕ID int bangumiID = 0; // 播放器实体 Player? mediaPlayer; VideoController? videoController; // 播放器面板状态 @observable bool loading = true; @observable bool playing = false; @observable bool isBuffering = true; @observable bool completed = false; @observable Duration currentPosition = Duration.zero; @observable Duration buffer = Duration.zero; @observable Duration duration = Duration.zero; @observable double playerSpeed = 1.0; Box setting = GStorage.setting; bool hAenable = true; late String hardwareDecoder; bool androidEnableOpenSLES = true; bool lowMemoryMode = false; bool autoPlay = true; bool playerDebugMode = false; int buttonSkipTime = 80; int arrowKeySkipTime = 10; // 播放器实时状态 bool get playerPlaying => mediaPlayer!.state.playing; bool get playerBuffering => mediaPlayer!.state.buffering; bool get playerCompleted => mediaPlayer!.state.completed; double get playerVolume => mediaPlayer!.state.volume; Duration get playerPosition => mediaPlayer!.state.position; Duration get playerBuffer => mediaPlayer!.state.buffer; Duration get playerDuration => mediaPlayer!.state.duration; // 播放器调试信息 /// LogLevel 0: 错误 1: 警告 2: 简略 3: 详细 int playerLogLevel = 2; @observable ObservableList playerLog = ObservableList.of([]); @observable int playerWidth = 0; @observable int playerHeight = 0; @observable String playerVideoParams = ''; @observable String playerAudioParams = ''; @observable String playerPlaylist = ''; @observable String playerAudioTracks = ''; @observable String playerVideoTracks = ''; @observable String playerAudioBitrate = ''; /// 播放器调试信息订阅 StreamSubscription? playerLogSubscription; StreamSubscription? playerWidthSubscription; StreamSubscription? playerHeightSubscription; StreamSubscription? playerVideoParamsSubscription; StreamSubscription? playerAudioParamsSubscription; StreamSubscription? playerPlaylistSubscription; StreamSubscription? playerTracksSubscription; StreamSubscription? playerAudioBitrateSubscription; bool isLocalPlayback = false; Future init(PlaybackInitParams params) async { videoUrl = params.videoUrl; isLocalPlayback = params.isLocalPlayback; bangumiId = params.bangumiId; currentEpisode = params.episode; currentRoad = params.currentRoad; referer = params.referer; KazumiLogger().i( 'PlayerController: ${params.isLocalPlayback ? "local" : "online"} playback, url: ${params.videoUrl}'); playing = false; loading = true; isBuffering = true; currentPosition = Duration.zero; buffer = Duration.zero; duration = Duration.zero; completed = false; playerLogLevel = setting.get(SettingBoxKey.playerLogLevel, defaultValue: 2); playerSpeed = setting.get(SettingBoxKey.defaultPlaySpeed, defaultValue: 1.0); aspectRatioType = setting.get(SettingBoxKey.defaultAspectRatioType, defaultValue: 1); buttonSkipTime = setting.get(SettingBoxKey.buttonSkipTime, defaultValue: 80); arrowKeySkipTime = setting.get(SettingBoxKey.arrowKeySkipTime, defaultValue: 10); try { await dispose(disposeSyncPlayController: false); } catch (_) {} int episodeFromTitle = 0; try { episodeFromTitle = Utils.extractEpisodeNumber(params.episodeTitle); } catch (e) { KazumiLogger().e( 'PlayerController: failed to extract episode number from title', error: e); } if (episodeFromTitle == 0) { episodeFromTitle = params.episode; } _loadDanmaku(params.bangumiId, params.pluginName, episodeFromTitle); mediaPlayer ??= await createVideoController( params.httpHeaders, params.adBlockerEnabled, offset: params.offset, ); if (Utils.isDesktop()) { volume = volume != -1 ? volume : 100; await setVolume(volume); } else { // mobile is using system volume, don't setVolume here, // or iOS will mute if system volume is too low (#732) await FlutterVolumeController.getVolume().then((value) { volume = (value ?? 0.0) * 100; }); } setPlaybackSpeed(playerSpeed); KazumiLogger().i('PlayerController: video initialized'); loading = false; if (syncplayController?.isConnected ?? false) { if (syncplayController!.currentFileName != "$bangumiId[$currentEpisode]") { setSyncPlayPlayingBangumi( forceSyncPlaying: true, forceSyncPosition: 0.0); } } } Future setupPlayerDebugInfoSubscription() async { await playerLogSubscription?.cancel(); playerLogSubscription = mediaPlayer!.stream.log.listen((event) { playerLog.add(event.toString()); if (playerDebugMode) { KazumiLogger().i("MPV: ${event.toString()}", forceLog: true); } }); await playerWidthSubscription?.cancel(); playerWidthSubscription = mediaPlayer!.stream.width.listen((event) { playerWidth = event ?? 0; }); await playerHeightSubscription?.cancel(); playerHeightSubscription = mediaPlayer!.stream.height.listen((event) { playerHeight = event ?? 0; }); await playerVideoParamsSubscription?.cancel(); playerVideoParamsSubscription = mediaPlayer!.stream.videoParams.listen((event) { playerVideoParams = event.toString(); }); await playerAudioParamsSubscription?.cancel(); playerAudioParamsSubscription = mediaPlayer!.stream.audioParams.listen((event) { playerAudioParams = event.toString(); }); await playerPlaylistSubscription?.cancel(); playerPlaylistSubscription = mediaPlayer!.stream.playlist.listen((event) { playerPlaylist = event.toString(); }); await playerTracksSubscription?.cancel(); playerTracksSubscription = mediaPlayer!.stream.track.listen((event) { playerAudioTracks = event.audio.toString(); playerVideoTracks = event.video.toString(); }); await playerAudioBitrateSubscription?.cancel(); playerAudioBitrateSubscription = mediaPlayer!.stream.audioBitrate.listen((event) { playerAudioBitrate = event.toString(); }); } Future cancelPlayerDebugInfoSubscription() async { await playerLogSubscription?.cancel(); await playerWidthSubscription?.cancel(); await playerHeightSubscription?.cancel(); await playerVideoParamsSubscription?.cancel(); await playerAudioParamsSubscription?.cancel(); await playerPlaylistSubscription?.cancel(); await playerTracksSubscription?.cancel(); await playerAudioBitrateSubscription?.cancel(); } Future createVideoController( Map httpHeaders, bool adBlockerEnabled, {int offset = 0}) async { superResolutionType = setting.get(SettingBoxKey.defaultSuperResolutionType, defaultValue: 1); hAenable = setting.get(SettingBoxKey.hAenable, defaultValue: true); androidEnableOpenSLES = setting.get(SettingBoxKey.androidEnableOpenSLES, defaultValue: true); hardwareDecoder = setting.get(SettingBoxKey.hardwareDecoder, defaultValue: 'auto-safe'); autoPlay = setting.get(SettingBoxKey.autoPlay, defaultValue: true); lowMemoryMode = setting.get(SettingBoxKey.lowMemoryMode, defaultValue: false); playerDebugMode = setting.get(SettingBoxKey.playerDebugMode, defaultValue: false); mediaPlayer = Player( configuration: PlayerConfiguration( bufferSize: lowMemoryMode ? 15 * 1024 * 1024 : 1500 * 1024 * 1024, osc: false, logLevel: MPVLogLevel.values[playerLogLevel], adBlocker: adBlockerEnabled, ), ); playerLog.clear(); setupPlayerDebugInfoSubscription(); var pp = mediaPlayer!.platform as NativePlayer; // media-kit 默认启用硬盘作为双重缓存,这可以维持大缓存的前提下减轻内存压力 // media-kit 内部硬盘缓存目录按照 Linux 配置,这导致该功能在其他平台上被损坏 // 该设置可以在所有平台上正确启用双重缓存 await pp.setProperty("demuxer-cache-dir", await Utils.getPlayerTempPath()); await pp.setProperty("af", "scaletempo2=max-speed=8"); if (Platform.isAndroid) { await pp.setProperty("volume-max", "100"); if (androidEnableOpenSLES) { await pp.setProperty("ao", "opensles"); } else { await pp.setProperty("ao", "audiotrack"); } } // 设置 HTTP 代理 final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (proxyEnable) { final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl); if (formattedProxy != null) { await pp.setProperty("http-proxy", formattedProxy); KazumiLogger().i('Player: HTTP 代理设置成功 $formattedProxy'); } } await mediaPlayer!.setAudioTrack( AudioTrack.auto(), ); String? videoRenderer; if (Platform.isAndroid) { final String androidVideoRenderer = setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto'); if (androidVideoRenderer == 'auto') { // Android 14 及以上使用基于 Vulkan 的 MPV GPU-NEXT 视频输出,着色器性能更好 // GPU-NEXT 需要 Vulkan 1.2 支持 // 避免 Android 14 及以下设备上部分机型 Vulkan 支持不佳导致的黑屏问题 final int androidSdkVersion = await Utils.getAndroidSdkVersion(); if (androidSdkVersion >= 34) { videoRenderer = 'gpu-next'; } else { videoRenderer = 'gpu'; } } else { videoRenderer = androidVideoRenderer; } } if (videoRenderer == 'mediacodec_embed') { hAenable = true; hardwareDecoder = 'mediacodec'; superResolutionType = 1; } videoController ??= VideoController( mediaPlayer!, configuration: VideoControllerConfiguration( vo: videoRenderer, enableHardwareAcceleration: hAenable, hwdec: hAenable ? hardwareDecoder : 'no', androidAttachSurfaceAfterVideoParameters: false, ), ); mediaPlayer!.setPlaylistMode(PlaylistMode.none); // error handle bool showPlayerError = setting.get(SettingBoxKey.showPlayerError, defaultValue: true); mediaPlayer!.stream.error.listen((event) { if (showPlayerError) { if (event.toString().contains('Failed to open') && playerBuffering) { KazumiDialog.showToast( message: '加载失败, 请尝试更换其他视频来源', showActionButton: true); } else { KazumiDialog.showToast( message: '播放器内部错误 ${event.toString()} $videoUrl', duration: const Duration(seconds: 5), showActionButton: true); } } KazumiLogger() .e('PlayerController: Player intent error $videoUrl', error: event); }); if (superResolutionType != 1) { await setShader(superResolutionType); } await mediaPlayer!.open( Media(videoUrl, start: Duration(seconds: offset), httpHeaders: httpHeaders), play: autoPlay, ); return mediaPlayer!; } Future setShader(int type, {bool synchronized = true}) async { var pp = mediaPlayer!.platform as NativePlayer; await pp.waitForPlayerInitialization; await pp.waitForVideoControllerInitializationIfAttached; if (type == 2) { await pp.command([ 'change-list', 'glsl-shaders', 'set', Utils.buildShadersAbsolutePath( shadersController.shadersDirectory.path, mpvAnime4KShadersLite), ]); superResolutionType = 2; return; } if (type == 3) { await pp.command([ 'change-list', 'glsl-shaders', 'set', Utils.buildShadersAbsolutePath( shadersController.shadersDirectory.path, mpvAnime4KShaders), ]); superResolutionType = 3; return; } await pp.command(['change-list', 'glsl-shaders', 'clr', '']); superResolutionType = 1; } Future setPlaybackSpeed(double playerSpeed) async { this.playerSpeed = playerSpeed; try { mediaPlayer!.setRate(playerSpeed); } catch (e) { KazumiLogger() .e('PlayerController: failed to set playback speed', error: e); } try { updateDanmakuSpeed(); } catch (_) {} } void updateDanmakuSpeed() { final baseDuration = setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0); final followSpeed = setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true); final duration = followSpeed ? (baseDuration / playerSpeed) : baseDuration; danmakuController .updateOption(danmakuController.option.copyWith(duration: duration)); } Future setVolume(double value) async { value = value.clamp(0.0, 100.0); volume = value; try { if (Utils.isDesktop()) { await mediaPlayer!.setVolume(value); } else { await FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(value / 100); } } catch (_) {} } Future playOrPause() async { if (mediaPlayer!.state.playing) { await pause(); } else { await play(); } } Future seek(Duration duration, {bool enableSync = true}) async { currentPosition = duration; danmakuController.clear(); await mediaPlayer!.seek(duration); if (syncplayController != null) { setSyncPlayCurrentPosition(); if (enableSync) { await requestSyncPlaySync(doSeek: true); } } } Future pause({bool enableSync = true}) async { danmakuController.pause(); await mediaPlayer!.pause(); playing = false; if (syncplayController != null) { setSyncPlayCurrentPosition(); if (enableSync) { await requestSyncPlaySync(); } } } Future play({bool enableSync = true}) async { danmakuController.resume(); await mediaPlayer!.play(); playing = true; if (syncplayController != null) { setSyncPlayCurrentPosition(); if (enableSync) { await requestSyncPlaySync(); } } } Future dispose({bool disposeSyncPlayController = true}) async { if (disposeSyncPlayController) { try { syncplayRoom = ''; syncplayClientRtt = 0; await syncplayController?.disconnect(); syncplayController = null; } catch (_) {} } try { await cancelPlayerDebugInfoSubscription(); } catch (_) {} await mediaPlayer?.dispose(); mediaPlayer = null; videoController = null; } Future stop() async { try { await mediaPlayer?.stop(); loading = true; } catch (_) {} } Future screenshot({String format = 'image/jpeg'}) async { return await mediaPlayer!.screenshot(format: format); } void setButtonForwardTime(int time) { buttonSkipTime = time; setting.put(SettingBoxKey.buttonSkipTime, time); } void setArrowKeyForwardTime(int time) { arrowKeySkipTime = time; setting.put(SettingBoxKey.arrowKeySkipTime, time); } /// 加载弹幕 (离线模式优先从缓存加载,无缓存时尝试在线获取) Future _loadDanmaku( int bangumiId, String pluginName, int episode) async { if (isLocalPlayback) { await _loadCachedDanmaku(bangumiId, pluginName, episode); } else { getDanDanmakuByBgmBangumiID(bangumiId, episode); } } Future _loadCachedDanmaku( int bangumiId, String pluginName, int episode) async { if (danmakuLoading) { KazumiLogger() .i('PlayerController: danmaku is loading, ignore duplicate request'); return; } KazumiLogger().i( 'PlayerController: attempting to load cached danmaku for episode $episode'); danmakuLoading = true; try { danDanmakus.clear(); final downloadController = Modular.get(); final cachedDanmakus = await downloadController.getCachedDanmakus( bangumiId, pluginName, episode, ); if (cachedDanmakus != null && cachedDanmakus.isNotEmpty) { addDanmakus(cachedDanmakus); KazumiLogger().i( 'PlayerController: loaded ${cachedDanmakus.length} cached danmakus'); } else { KazumiLogger() .i('PlayerController: no cached danmaku, attempting online fetch'); try { bangumiID = await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bangumiId); if (bangumiID != 0) { var res = await DanmakuRequest.getDanDanmaku(bangumiID, episode); if (res.isNotEmpty) { addDanmakus(res); KazumiLogger() .i('PlayerController: fetched ${res.length} danmakus online'); _saveDanmakuToCache( downloadController, bangumiId, pluginName, episode, res); } } } catch (e) { KazumiLogger().w( 'PlayerController: failed to fetch danmaku online (may be offline)', error: e); } } } catch (e) { KazumiLogger() .w('PlayerController: failed to load cached danmaku', error: e); } finally { danmakuLoading = false; } } void _saveDanmakuToCache(DownloadController downloadController, int bangumiId, String pluginName, int episode, List danmakus) { try { downloadController.updateCachedDanmakus( bangumiId, pluginName, episode, danmakus, bangumiID, ); KazumiLogger() .i('PlayerController: saved ${danmakus.length} danmakus to cache'); } catch (e) { KazumiLogger() .w('PlayerController: failed to save danmaku to cache', error: e); } } Future getDanDanmakuByBgmBangumiID( int bgmBangumiID, int episode) async { if (danmakuLoading) { KazumiLogger() .i('PlayerController: danmaku is loading, ignore duplicate request'); return; } KazumiLogger().i( 'PlayerController: attempting to get danmaku [BgmBangumiID] $bgmBangumiID'); danmakuLoading = true; try { danDanmakus.clear(); bangumiID = await DanmakuRequest.getDanDanBangumiIDByBgmBangumiID(bgmBangumiID); var res = await DanmakuRequest.getDanDanmaku(bangumiID, episode); addDanmakus(res); } catch (e) { KazumiLogger().w( 'PlayerController: failed to get danmaku [BgmBangumiID] $bgmBangumiID', error: e); } finally { danmakuLoading = false; } } Future getDanDanmakuByEpisodeID(int episodeID) async { if (danmakuLoading) { KazumiLogger() .i('PlayerController: danmaku is loading, ignore duplicate request'); return; } KazumiLogger().i('PlayerController: attempting to get danmaku $episodeID'); danmakuLoading = true; try { danDanmakus.clear(); var res = await DanmakuRequest.getDanDanmakuByEpisodeID(episodeID); addDanmakus(res); } catch (e) { KazumiLogger().w('PlayerController: failed to get danmaku', error: e); } finally { danmakuLoading = false; } } void addDanmakus(List danmakus) { final bool danmakuDeduplicationEnable = setting.get(SettingBoxKey.danmakuDeduplication, defaultValue: false); // 如果启用了弹幕去重功能则处理5秒内相邻重复类似的弹幕进行合并 final List listToAdd = danmakuDeduplicationEnable ? Utils.mergeDuplicateDanmakus(danmakus, timeWindowSeconds: 5) : danmakus; for (var element in listToAdd) { var danmakuList = danDanmakus[element.time.toInt()] ?? List.empty(growable: true); danmakuList.add(element); danDanmakus[element.time.toInt()] = danmakuList; } } void lanunchExternalPlayer() async { if ((Platform.isAndroid || Platform.isWindows) && referer.isEmpty) { if (await ExternalPlayer.launchURLWithMIME(videoUrl, 'video/mp4')) { KazumiDialog.dismiss(); KazumiDialog.showToast( message: '尝试唤起外部播放器', ); } else { KazumiDialog.showToast( message: '唤起外部播放器失败', ); } } else if (Platform.isMacOS || Platform.isIOS) { if (await ExternalPlayer.launchURLWithReferer(videoUrl, referer)) { KazumiDialog.dismiss(); KazumiDialog.showToast( message: '尝试唤起外部播放器', ); } else { KazumiDialog.showToast( message: '唤起外部播放器失败', ); } } else if (Platform.isLinux && referer.isEmpty) { KazumiDialog.dismiss(); if (await canLaunchUrlString(videoUrl)) { launchUrlString(videoUrl); KazumiDialog.showToast( message: '尝试唤起外部播放器', ); } else { KazumiDialog.showToast( message: '无法使用外部播放器', ); } } else { if (referer.isEmpty) { KazumiDialog.showToast( message: '暂不支持该设备', ); } else { KazumiDialog.showToast( message: '暂不支持该规则', ); } } } Future createSyncPlayRoom( String room, String username, Future Function(int episode, {int currentRoad, int offset}) changeEpisode, {bool enableTLS = true}) async { await syncplayController?.disconnect(); final String syncPlayEndPoint = setting.get(SettingBoxKey.syncPlayEndPoint, defaultValue: defaultSyncPlayEndPoint); String syncPlayEndPointHost = ''; int syncPlayEndPointPort = 0; KazumiLogger().i('SyncPlay: connecting to $syncPlayEndPoint'); try { final parsed = parseSyncPlayEndPoint(syncPlayEndPoint); if (parsed != null) { syncPlayEndPointHost = parsed.host; syncPlayEndPointPort = parsed.port; } } catch (_) {} if (syncPlayEndPointHost == '' || syncPlayEndPointPort == 0) { KazumiDialog.showToast( message: 'SyncPlay: 服务器地址不合法 $syncPlayEndPoint', ); KazumiLogger().e('SyncPlay: invalid server address $syncPlayEndPoint'); return; } syncplayController = SyncplayClient(host: syncPlayEndPointHost, port: syncPlayEndPointPort); try { await syncplayController!.connect(enableTLS: enableTLS); KazumiLogger().i( 'SyncPlay: connected to $syncPlayEndPointHost:$syncPlayEndPointPort'); syncplayController!.onGeneralMessage.listen( (message) { // print('SyncPlay: general message: ${message.toString()}'); }, onError: (error) { print('SyncPlay: error: ${error.message}'); if (error is SyncplayConnectionException) { exitSyncPlayRoom(); KazumiDialog.showToast( message: 'SyncPlay: 同步中断 ${error.message}', duration: const Duration(seconds: 5), showActionButton: true, actionLabel: '重新连接', onActionPressed: () => createSyncPlayRoom(room, username, changeEpisode), ); } }, ); syncplayController!.onRoomMessage.listen( (message) { if (message['type'] == 'init') { if (message['username'] == '') { KazumiDialog.showToast( message: 'SyncPlay: 您是当前房间中的唯一用户', duration: const Duration(seconds: 5)); setSyncPlayPlayingBangumi(); } else { KazumiDialog.showToast( message: 'SyncPlay: 您不是当前房间中的唯一用户, 当前以用户 ${message['username']} 进度为准'); } } if (message['type'] == 'left') { KazumiDialog.showToast( message: 'SyncPlay: ${message['username']} 离开了房间', duration: const Duration(seconds: 5)); } if (message['type'] == 'joined') { KazumiDialog.showToast( message: 'SyncPlay: ${message['username']} 加入了房间', duration: const Duration(seconds: 5)); } }, ); syncplayController!.onFileChangedMessage.listen( (message) { print( 'SyncPlay: file changed by ${message['setBy']}: ${message['name']}'); RegExp regExp = RegExp(r'(\d+)\[(\d+)\]'); Match? match = regExp.firstMatch(message['name']); if (match != null) { int bangumiID = int.tryParse(match.group(1) ?? '0') ?? 0; int episode = int.tryParse(match.group(2) ?? '0') ?? 0; if (bangumiID != 0 && episode != 0 && episode != currentEpisode) { KazumiDialog.showToast( message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 切换到第 $episode 话', duration: const Duration(seconds: 3)); changeEpisode(episode, currentRoad: currentRoad); } } }, ); syncplayController!.onChatMessage.listen( (message) { final String sender = (message['username'] ?? '').toString(); final String text = (message['message'] ?? '').toString(); final bool fromRemote = message['username'] != username; // 将消息转发到流 if (!syncPlayChatStreamController.isClosed) { syncPlayChatStreamController.add(SyncPlayChatMessage( username: sender, message: text, fromRemote: fromRemote, )); } }, onError: (error) { print('SyncPlay: error: ${error.message}'); }, ); syncplayController!.onPositionChangedMessage.listen( (message) { syncplayClientRtt = (message['clientRtt'].toDouble() * 1000).toInt(); print( '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']}'); if (message['paused'] != !playing) { if (message['paused']) { if (message['position'] != 0) { KazumiDialog.showToast( message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 暂停了播放', duration: const Duration(seconds: 3)); pause(enableSync: false); } } else { if (message['position'] != 0) { KazumiDialog.showToast( message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 开始了播放', duration: const Duration(seconds: 3)); play(enableSync: false); } } } if ((((playerPosition.inMilliseconds - (message['calculatedPositon'].toDouble() * 1000) .toInt()) .abs() > 1000) || message['doSeek']) && duration.inMilliseconds > 0) { seek( Duration( milliseconds: (message['calculatedPositon'].toDouble() * 1000) .toInt()), enableSync: false); } }, ); await syncplayController!.joinRoom(room, username); syncplayRoom = room; } catch (e) { print('SyncPlay: error: $e'); } } void setSyncPlayCurrentPosition( {bool? forceSyncPlaying, double? forceSyncPosition}) { if (syncplayController == null) { return; } forceSyncPlaying ??= playing; syncplayController!.setPaused(!forceSyncPlaying); syncplayController!.setPosition((forceSyncPosition ?? (((currentPosition.inMilliseconds - playerPosition.inMilliseconds) .abs() > 2000) ? currentPosition.inMilliseconds.toDouble() / 1000 : playerPosition.inMilliseconds.toDouble() / 1000))); } Future setSyncPlayPlayingBangumi( {bool? forceSyncPlaying, double? forceSyncPosition}) async { await syncplayController! .setSyncPlayPlaying("$bangumiId[$currentEpisode]", 10800, 220514438); setSyncPlayCurrentPosition( forceSyncPlaying: forceSyncPlaying, forceSyncPosition: forceSyncPosition); await requestSyncPlaySync(); } Future requestSyncPlaySync({bool? doSeek}) async { await syncplayController!.sendSyncPlaySyncRequest(doSeek: doSeek); } Future sendSyncPlayChatMessage(String message) async { if (syncplayController == null) { return; } await syncplayController!.sendChatMessage(message); } Future exitSyncPlayRoom() async { if (syncplayController == null) { return; } await syncplayController!.disconnect(); syncplayController = null; syncplayRoom = ''; syncplayClientRtt = 0; } } ================================================ FILE: lib/pages/player/player_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'player_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$PlayerController on _PlayerController, Store { late final _$danDanmakusAtom = Atom(name: '_PlayerController.danDanmakus', context: context); @override Map> get danDanmakus { _$danDanmakusAtom.reportRead(); return super.danDanmakus; } @override set danDanmakus(Map> value) { _$danDanmakusAtom.reportWrite(value, super.danDanmakus, () { super.danDanmakus = value; }); } late final _$danmakuOnAtom = Atom(name: '_PlayerController.danmakuOn', context: context); @override bool get danmakuOn { _$danmakuOnAtom.reportRead(); return super.danmakuOn; } @override set danmakuOn(bool value) { _$danmakuOnAtom.reportWrite(value, super.danmakuOn, () { super.danmakuOn = value; }); } late final _$danmakuLoadingAtom = Atom(name: '_PlayerController.danmakuLoading', context: context); @override bool get danmakuLoading { _$danmakuLoadingAtom.reportRead(); return super.danmakuLoading; } @override set danmakuLoading(bool value) { _$danmakuLoadingAtom.reportWrite(value, super.danmakuLoading, () { super.danmakuLoading = value; }); } late final _$syncplayRoomAtom = Atom(name: '_PlayerController.syncplayRoom', context: context); @override String get syncplayRoom { _$syncplayRoomAtom.reportRead(); return super.syncplayRoom; } @override set syncplayRoom(String value) { _$syncplayRoomAtom.reportWrite(value, super.syncplayRoom, () { super.syncplayRoom = value; }); } late final _$syncplayClientRttAtom = Atom(name: '_PlayerController.syncplayClientRtt', context: context); @override int get syncplayClientRtt { _$syncplayClientRttAtom.reportRead(); return super.syncplayClientRtt; } @override set syncplayClientRtt(int value) { _$syncplayClientRttAtom.reportWrite(value, super.syncplayClientRtt, () { super.syncplayClientRtt = value; }); } late final _$aspectRatioTypeAtom = Atom(name: '_PlayerController.aspectRatioType', context: context); @override int get aspectRatioType { _$aspectRatioTypeAtom.reportRead(); return super.aspectRatioType; } @override set aspectRatioType(int value) { _$aspectRatioTypeAtom.reportWrite(value, super.aspectRatioType, () { super.aspectRatioType = value; }); } late final _$superResolutionTypeAtom = Atom(name: '_PlayerController.superResolutionType', context: context); @override int get superResolutionType { _$superResolutionTypeAtom.reportRead(); return super.superResolutionType; } @override set superResolutionType(int value) { _$superResolutionTypeAtom.reportWrite(value, super.superResolutionType, () { super.superResolutionType = value; }); } late final _$volumeAtom = Atom(name: '_PlayerController.volume', context: context); @override double get volume { _$volumeAtom.reportRead(); return super.volume; } @override set volume(double value) { _$volumeAtom.reportWrite(value, super.volume, () { super.volume = value; }); } late final _$brightnessAtom = Atom(name: '_PlayerController.brightness', context: context); @override double get brightness { _$brightnessAtom.reportRead(); return super.brightness; } @override set brightness(double value) { _$brightnessAtom.reportWrite(value, super.brightness, () { super.brightness = value; }); } late final _$lockPanelAtom = Atom(name: '_PlayerController.lockPanel', context: context); @override bool get lockPanel { _$lockPanelAtom.reportRead(); return super.lockPanel; } @override set lockPanel(bool value) { _$lockPanelAtom.reportWrite(value, super.lockPanel, () { super.lockPanel = value; }); } late final _$showVideoControllerAtom = Atom(name: '_PlayerController.showVideoController', context: context); @override bool get showVideoController { _$showVideoControllerAtom.reportRead(); return super.showVideoController; } @override set showVideoController(bool value) { _$showVideoControllerAtom.reportWrite(value, super.showVideoController, () { super.showVideoController = value; }); } late final _$showSeekTimeAtom = Atom(name: '_PlayerController.showSeekTime', context: context); @override bool get showSeekTime { _$showSeekTimeAtom.reportRead(); return super.showSeekTime; } @override set showSeekTime(bool value) { _$showSeekTimeAtom.reportWrite(value, super.showSeekTime, () { super.showSeekTime = value; }); } late final _$showBrightnessAtom = Atom(name: '_PlayerController.showBrightness', context: context); @override bool get showBrightness { _$showBrightnessAtom.reportRead(); return super.showBrightness; } @override set showBrightness(bool value) { _$showBrightnessAtom.reportWrite(value, super.showBrightness, () { super.showBrightness = value; }); } late final _$showVolumeAtom = Atom(name: '_PlayerController.showVolume', context: context); @override bool get showVolume { _$showVolumeAtom.reportRead(); return super.showVolume; } @override set showVolume(bool value) { _$showVolumeAtom.reportWrite(value, super.showVolume, () { super.showVolume = value; }); } late final _$showPlaySpeedAtom = Atom(name: '_PlayerController.showPlaySpeed', context: context); @override bool get showPlaySpeed { _$showPlaySpeedAtom.reportRead(); return super.showPlaySpeed; } @override set showPlaySpeed(bool value) { _$showPlaySpeedAtom.reportWrite(value, super.showPlaySpeed, () { super.showPlaySpeed = value; }); } late final _$brightnessSeekingAtom = Atom(name: '_PlayerController.brightnessSeeking', context: context); @override bool get brightnessSeeking { _$brightnessSeekingAtom.reportRead(); return super.brightnessSeeking; } @override set brightnessSeeking(bool value) { _$brightnessSeekingAtom.reportWrite(value, super.brightnessSeeking, () { super.brightnessSeeking = value; }); } late final _$volumeSeekingAtom = Atom(name: '_PlayerController.volumeSeeking', context: context); @override bool get volumeSeeking { _$volumeSeekingAtom.reportRead(); return super.volumeSeeking; } @override set volumeSeeking(bool value) { _$volumeSeekingAtom.reportWrite(value, super.volumeSeeking, () { super.volumeSeeking = value; }); } late final _$canHidePlayerPanelAtom = Atom(name: '_PlayerController.canHidePlayerPanel', context: context); @override bool get canHidePlayerPanel { _$canHidePlayerPanelAtom.reportRead(); return super.canHidePlayerPanel; } @override set canHidePlayerPanel(bool value) { _$canHidePlayerPanelAtom.reportWrite(value, super.canHidePlayerPanel, () { super.canHidePlayerPanel = value; }); } late final _$loadingAtom = Atom(name: '_PlayerController.loading', context: context); @override bool get loading { _$loadingAtom.reportRead(); return super.loading; } @override set loading(bool value) { _$loadingAtom.reportWrite(value, super.loading, () { super.loading = value; }); } late final _$playingAtom = Atom(name: '_PlayerController.playing', context: context); @override bool get playing { _$playingAtom.reportRead(); return super.playing; } @override set playing(bool value) { _$playingAtom.reportWrite(value, super.playing, () { super.playing = value; }); } late final _$isBufferingAtom = Atom(name: '_PlayerController.isBuffering', context: context); @override bool get isBuffering { _$isBufferingAtom.reportRead(); return super.isBuffering; } @override set isBuffering(bool value) { _$isBufferingAtom.reportWrite(value, super.isBuffering, () { super.isBuffering = value; }); } late final _$completedAtom = Atom(name: '_PlayerController.completed', context: context); @override bool get completed { _$completedAtom.reportRead(); return super.completed; } @override set completed(bool value) { _$completedAtom.reportWrite(value, super.completed, () { super.completed = value; }); } late final _$currentPositionAtom = Atom(name: '_PlayerController.currentPosition', context: context); @override Duration get currentPosition { _$currentPositionAtom.reportRead(); return super.currentPosition; } @override set currentPosition(Duration value) { _$currentPositionAtom.reportWrite(value, super.currentPosition, () { super.currentPosition = value; }); } late final _$bufferAtom = Atom(name: '_PlayerController.buffer', context: context); @override Duration get buffer { _$bufferAtom.reportRead(); return super.buffer; } @override set buffer(Duration value) { _$bufferAtom.reportWrite(value, super.buffer, () { super.buffer = value; }); } late final _$durationAtom = Atom(name: '_PlayerController.duration', context: context); @override Duration get duration { _$durationAtom.reportRead(); return super.duration; } @override set duration(Duration value) { _$durationAtom.reportWrite(value, super.duration, () { super.duration = value; }); } late final _$playerSpeedAtom = Atom(name: '_PlayerController.playerSpeed', context: context); @override double get playerSpeed { _$playerSpeedAtom.reportRead(); return super.playerSpeed; } @override set playerSpeed(double value) { _$playerSpeedAtom.reportWrite(value, super.playerSpeed, () { super.playerSpeed = value; }); } late final _$playerLogAtom = Atom(name: '_PlayerController.playerLog', context: context); @override ObservableList get playerLog { _$playerLogAtom.reportRead(); return super.playerLog; } @override set playerLog(ObservableList value) { _$playerLogAtom.reportWrite(value, super.playerLog, () { super.playerLog = value; }); } late final _$playerWidthAtom = Atom(name: '_PlayerController.playerWidth', context: context); @override int get playerWidth { _$playerWidthAtom.reportRead(); return super.playerWidth; } @override set playerWidth(int value) { _$playerWidthAtom.reportWrite(value, super.playerWidth, () { super.playerWidth = value; }); } late final _$playerHeightAtom = Atom(name: '_PlayerController.playerHeight', context: context); @override int get playerHeight { _$playerHeightAtom.reportRead(); return super.playerHeight; } @override set playerHeight(int value) { _$playerHeightAtom.reportWrite(value, super.playerHeight, () { super.playerHeight = value; }); } late final _$playerVideoParamsAtom = Atom(name: '_PlayerController.playerVideoParams', context: context); @override String get playerVideoParams { _$playerVideoParamsAtom.reportRead(); return super.playerVideoParams; } @override set playerVideoParams(String value) { _$playerVideoParamsAtom.reportWrite(value, super.playerVideoParams, () { super.playerVideoParams = value; }); } late final _$playerAudioParamsAtom = Atom(name: '_PlayerController.playerAudioParams', context: context); @override String get playerAudioParams { _$playerAudioParamsAtom.reportRead(); return super.playerAudioParams; } @override set playerAudioParams(String value) { _$playerAudioParamsAtom.reportWrite(value, super.playerAudioParams, () { super.playerAudioParams = value; }); } late final _$playerPlaylistAtom = Atom(name: '_PlayerController.playerPlaylist', context: context); @override String get playerPlaylist { _$playerPlaylistAtom.reportRead(); return super.playerPlaylist; } @override set playerPlaylist(String value) { _$playerPlaylistAtom.reportWrite(value, super.playerPlaylist, () { super.playerPlaylist = value; }); } late final _$playerAudioTracksAtom = Atom(name: '_PlayerController.playerAudioTracks', context: context); @override String get playerAudioTracks { _$playerAudioTracksAtom.reportRead(); return super.playerAudioTracks; } @override set playerAudioTracks(String value) { _$playerAudioTracksAtom.reportWrite(value, super.playerAudioTracks, () { super.playerAudioTracks = value; }); } late final _$playerVideoTracksAtom = Atom(name: '_PlayerController.playerVideoTracks', context: context); @override String get playerVideoTracks { _$playerVideoTracksAtom.reportRead(); return super.playerVideoTracks; } @override set playerVideoTracks(String value) { _$playerVideoTracksAtom.reportWrite(value, super.playerVideoTracks, () { super.playerVideoTracks = value; }); } late final _$playerAudioBitrateAtom = Atom(name: '_PlayerController.playerAudioBitrate', context: context); @override String get playerAudioBitrate { _$playerAudioBitrateAtom.reportRead(); return super.playerAudioBitrate; } @override set playerAudioBitrate(String value) { _$playerAudioBitrateAtom.reportWrite(value, super.playerAudioBitrate, () { super.playerAudioBitrate = value; }); } @override String toString() { return ''' danDanmakus: ${danDanmakus}, danmakuOn: ${danmakuOn}, danmakuLoading: ${danmakuLoading}, syncplayRoom: ${syncplayRoom}, syncplayClientRtt: ${syncplayClientRtt}, aspectRatioType: ${aspectRatioType}, superResolutionType: ${superResolutionType}, volume: ${volume}, brightness: ${brightness}, lockPanel: ${lockPanel}, showVideoController: ${showVideoController}, showSeekTime: ${showSeekTime}, showBrightness: ${showBrightness}, showVolume: ${showVolume}, showPlaySpeed: ${showPlaySpeed}, brightnessSeeking: ${brightnessSeeking}, volumeSeeking: ${volumeSeeking}, canHidePlayerPanel: ${canHidePlayerPanel}, loading: ${loading}, playing: ${playing}, isBuffering: ${isBuffering}, completed: ${completed}, currentPosition: ${currentPosition}, buffer: ${buffer}, duration: ${duration}, playerSpeed: ${playerSpeed}, playerLog: ${playerLog}, playerWidth: ${playerWidth}, playerHeight: ${playerHeight}, playerVideoParams: ${playerVideoParams}, playerAudioParams: ${playerAudioParams}, playerPlaylist: ${playerPlaylist}, playerAudioTracks: ${playerAudioTracks}, playerVideoTracks: ${playerVideoTracks}, playerAudioBitrate: ${playerAudioBitrate} '''; } } ================================================ FILE: lib/pages/player/player_item.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:kazumi/pages/player/player_item_panel.dart'; import 'package:kazumi/pages/player/smallest_player_item_panel.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/webdav.dart'; import 'package:flutter/services.dart'; import 'package:flutter/gestures.dart'; import 'package:kazumi/pages/player/player_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:window_manager/window_manager.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/request/damaku.dart'; import 'package:kazumi/modules/danmaku/danmaku_search_response.dart'; import 'package:kazumi/modules/danmaku/danmaku_episode_response.dart'; import 'package:kazumi/pages/player/player_item_surface.dart'; import 'package:mobx/mobx.dart' as mobx; import 'package:kazumi/pages/my/my_controller.dart'; import 'package:saver_gallery/saver_gallery.dart'; class PlayerItem extends StatefulWidget { const PlayerItem({ super.key, required this.openMenu, required this.locateEpisode, required this.changeEpisode, required this.onBackPressed, required this.keyboardFocus, required this.sendDanmaku, required this.showDanmakuDestinationPickerAndSend, required this.pauseForTimedShutdown, this.disableAnimations = false, }); final VoidCallback openMenu; final VoidCallback locateEpisode; final Future Function(int episode, {int currentRoad, int offset}) changeEpisode; final void Function(BuildContext) onBackPressed; final void Function(String) sendDanmaku; final FocusNode keyboardFocus; final bool disableAnimations; final void Function(String) showDanmakuDestinationPickerAndSend; final VoidCallback pauseForTimedShutdown; @override State createState() => _PlayerItemState(); } class _PlayerItemState extends State with WindowListener, WidgetsBindingObserver, SingleTickerProviderStateMixin { Box setting = GStorage.setting; final PlayerController playerController = Modular.get(); final VideoPageController videoPageController = Modular.get(); final HistoryController historyController = Modular.get(); final CollectController collectController = Modular.get(); final MyController myController = Modular.get(); late Map> keyboardShortcuts; late List keyboardActionsNeedLongPress; late Map keyboardActions; // 1. 在看 // 2. 想看 // 3. 搁置 // 4. 看过 // 5. 抛弃 late int collectType; late bool webDavEnable; late bool webDavEnableHistory; // 弹幕 final _danmuKey = GlobalKey(); late bool _border; late double _opacity; late double _fontSize; late double _danmakuArea; late bool _hideTop; late bool _hideBottom; late bool _hideScroll; late bool _massiveMode; late bool _danmakuColor; late bool _danmakuBiliBiliSource; late bool _danmakuGamerSource; late bool _danmakuDanDanSource; late double _danmakuDuration; late double _danmakuLineHeight; late int _danmakuFontWeight; late bool _danmakuUseSystemFont; late double _danmakuBorderSize; // 硬件解码 late bool haEnable; late bool autoPlayNext; Timer? hideTimer; Timer? playerTimer; Timer? mouseScrollerTimer; Timer? hideVolumeUITimer; double lastVolume = 0; // 过渡动画控制器 AnimationController? animationController; double lastPlayerSpeed = 1.0; int episodeNum = 0; late mobx.ReactionDisposer _fullscreenListener; /// 处理 Android/iOS 应用后台或熄屏 @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); try { if (playerController.playerPlaying) { playerController.danmakuController.resume(); } } catch (_) {} } void _loadShortcuts() { keyboardShortcuts = {}; defaultShortcuts.forEach((key, defaultValue) { keyboardShortcuts[key] = setting .get('shortcut_$key', defaultValue: defaultValue) .cast(); }); } void _initKeyboardActions() { //需要实现长按的功能列表。 keyboardActionsNeedLongPress = ["forward"]; //快捷键功能对应表 keyboardActions = { 'playorpause': () => playerController.playOrPause(), 'forward': () async => handleShortcutForwardDown(), 'rewind': () async => handleShortcutRewind(), 'next': () async => handlePreNextEpisode('next'), 'prev': () async => handlePreNextEpisode('prev'), 'volumeup': () async => handleShortcutVolumeChange('up'), 'volumedown': () async => handleShortcutVolumeChange('down'), 'togglemute': () async => handleShortcutVolumeChange('mute'), 'fullscreen': () => handleShortcutFullscreen(), 'screenshot': () async => handleScreenshot(), 'skip': () async => skipOP(), 'exitfullscreen': () => handleShortcutExitFullscreen(), 'toggledanmaku': () => handleDanmaku(), 'speed1': () async => setPlaybackSpeed(1.0), 'speed2': () async => setPlaybackSpeed(2.0), 'speed3': () async => setPlaybackSpeed(3.0), 'speedup': () async => handleSpeedChange('up'), 'speeddown': () async => handleSpeedChange('down'), // 开始对应长按功能 // 如需对应长按功能,例如对功能'func'对应长按,请分别添加'funcRepeat'和'funcUp'。 'forwardRepeat': () async => handleShortcutForwardRepeat(), 'forwardUp': () async => handleShortcutForwardUp(), }; } //初始化播放器菜单 void _initPlayerMenu() { Utils.initPlayerMenu(keyboardActions); } //销毁播放器菜单 void _disposePlayerMenu() { Utils.disposePlayerMenu(); } //快捷键按下 bool handleShortcutDown(String keyLabel) { for (final entry in keyboardShortcuts.entries) { final func = entry.key; final keys = entry.value; if (keys.contains(keyLabel)) { final action = keyboardActions[func]; if (action != null) { action(); return true; } } } return false; } // 快捷键长按 bool handleShortcutLongPress(String keyLabel, String mode) { for (final func in keyboardActionsNeedLongPress) { final keys = keyboardShortcuts[func]; if (keys?.contains(keyLabel) == true) { final action = keyboardActions[func + mode]; if (action != null) { action(); return true; } } } return false; } //上一集下一集动作 Future handlePreNextEpisode(String direction) async { if (videoPageController.loading) return; final currentRoad = videoPageController.currentRoad; final episodes = videoPageController.roadList[currentRoad].data; int targetEpisode; if (direction == 'next') { targetEpisode = videoPageController.currentEpisode + 1; } else if (direction == 'prev') { targetEpisode = videoPageController.currentEpisode - 1; } else { return; } if (targetEpisode > episodes.length) { KazumiDialog.showToast(message: '已经是最新一集'); return; } if (targetEpisode <= 0) { KazumiDialog.showToast(message: '已经是第一集'); return; } final identifier = videoPageController.roadList[currentRoad].identifier[targetEpisode - 1]; KazumiDialog.showToast(message: '正在加载$identifier'); widget.changeEpisode(targetEpisode, currentRoad: currentRoad); } //快退快捷键动作 Future handleShortcutRewind() async { int skipTime = playerController.arrowKeySkipTime; int current = playerController.currentPosition.inSeconds; int targetPosition; targetPosition = current - skipTime; if (targetPosition < 0) targetPosition = 0; try { playerTimer?.cancel(); await playerController.seek(Duration(seconds: targetPosition)); playerTimer = getPlayerTimer(); } catch (e) { KazumiLogger().e('PlayerController: seek failed', error: e); } } // 快进快捷键动作 Future handleShortcutForwardDown() async { lastPlayerSpeed = playerController.playerSpeed; } Future handleShortcutForwardRepeat() async { final double defaultShortcutForwardPlaySpeed = setting.get(SettingBoxKey.defaultShortcutForwardPlaySpeed, defaultValue: 2.0); if (playerController.playerSpeed < defaultShortcutForwardPlaySpeed) { playerController.showPlaySpeed = true; setPlaybackSpeed(defaultShortcutForwardPlaySpeed); } } Future handleShortcutForwardUp() async { int skipTime = playerController.arrowKeySkipTime; int current = playerController.currentPosition.inSeconds; int total = playerController.duration.inSeconds; int targetPosition; targetPosition = current + skipTime; if (targetPosition > total) targetPosition = total; if (playerController.showPlaySpeed) { playerController.showPlaySpeed = false; setPlaybackSpeed(lastPlayerSpeed); } else { try { playerTimer?.cancel(); playerController.seek(Duration(seconds: targetPosition)); playerTimer = getPlayerTimer(); } catch (e) { KazumiLogger().e('PlayerController: seek failed', error: e); } } } //全屏快捷键动作 void handleShortcutFullscreen() { if (!videoPageController.isPip) handleFullscreen(); } //退出全屏快捷键动作 void handleShortcutExitFullscreen() { if (videoPageController.isFullscreen && !Utils.isTablet()) { try { playerController.danmakuController.clear(); } catch (_) {} Utils.exitFullScreen(); videoPageController.isFullscreen = !videoPageController.isFullscreen; } else if (!Platform.isMacOS) { playerController.pause(); windowManager.hide(); } } void _handleTap() { if (Utils.isDesktop()) { playerController.playOrPause(); } else { if (playerController.showVideoController) { hideVideoController(); } else { displayVideoController(); } } } void _handleDoubleTap() { if (Utils.isDesktop() && !videoPageController.isPip) { handleFullscreen(); } else { playerController.playOrPause(); } } void _handleHove() { if (!playerController.showVideoController) { displayVideoController(); } hideTimer?.cancel(); startHideTimer(); } void _handleMouseScroller() { playerController.showVolume = true; mouseScrollerTimer?.cancel(); mouseScrollerTimer = Timer(const Duration(seconds: 2), () { if (mounted) { playerController.showVolume = false; } mouseScrollerTimer = null; }); } //跳过指定秒数 Future skipOP() async { await playerController.seek(playerController.currentPosition + Duration(seconds: playerController.buttonSkipTime)); } void handleDanmaku() { playerController.danmakuController.clear(); // if true, turn off danmaku. if (playerController.danmakuOn) { setState(() { playerController.danmakuOn = false; }); setting.put(SettingBoxKey.danmakuEnabledByDefault, false); return; } // if false and empty, show dialog. if (playerController.danDanmakus.isEmpty) { showDanmakuSwitch(); return; } // turn on danmaku. setState(() { playerController.danmakuOn = true; }); setting.put(SettingBoxKey.danmakuEnabledByDefault, true); } Future _uploadHistoryToWebDav() async { if (webDavEnable && webDavEnableHistory) { try { var webDav = WebDav(); await webDav.updateHistory(); } catch (_) {} } } void _handleFullscreenChange(BuildContext context) async { playerController.lockPanel = false; playerController.danmakuController.clear(); await _uploadHistoryToWebDav(); } void handleProgressBarDragStart(ThumbDragDetails details) { playerTimer?.cancel(); playerController.pause(enableSync: false); hideTimer?.cancel(); playerController.showVideoController = true; } void handleProgressBarDragEnd() { playerController.play(enableSync: false); startHideTimer(); playerTimer?.cancel(); playerTimer = getPlayerTimer(); } //截图 Future handleScreenshot() async { KazumiDialog.showToast(message: '截图中...'); try { Uint8List? screenshot = await playerController.screenshot(format: 'image/png'); if (screenshot == null) { KazumiDialog.showToast(message: '截图失败:未获取到图像'); return; } if (Utils.isDesktop()) { KazumiDialog.showToast(message: '桌面端暂未支持保存截图'); return; } final result = await SaverGallery.saveImage( screenshot, fileName: DateTime.timestamp().millisecondsSinceEpoch.toString(), skipIfExists: false, ); if (result.isSuccess) { KazumiDialog.showToast(message: '截图保存到相簿成功'); } else { KazumiDialog.showToast(message: '截图保存失败:${result.errorMessage}'); } } catch (e) { KazumiDialog.showToast(message: '截图失败:$e'); } } // 启用超分辨率(质量档)时弹出提示 Future handleSuperResolutionChange(int shaderIndex) async { if (!mounted) return; // mediacodec_embed 不支持超分辨率 if (Platform.isAndroid && shaderIndex != 1) { final String androidVideoRenderer = setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto'); if (androidVideoRenderer == 'mediacodec_embed') { await KazumiDialog.show(builder: (context) { return AlertDialog( title: const Text('兼容性提示'), content: const Text('MediaCodec 渲染器不支持超分辨率功能。\n\n' '如需使用超分辨率,请在播放设置中将视频渲染器切换为 gpu 或 gpu-next。'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: const Text('确定'), ), ], ); }); return; } } final bool isHighMode = shaderIndex == 3; final bool alreadyShown = setting.get(SettingBoxKey.superResolutionWarn, defaultValue: false); if (isHighMode && !alreadyShown) { bool confirmed = false; await KazumiDialog.show(builder: (context) { bool dontAskAgain = false; return StatefulBuilder(builder: (context, setState) { return AlertDialog( title: const Text('性能提示'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('启用超分辨率(质量档)可能会造成设备卡顿,是否继续?'), const SizedBox(height: 12), Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: dontAskAgain, onChanged: (value) => setState(() => dontAskAgain = value ?? false), ), const Text('下次不再询问'), ], ), ], ), actions: [ TextButton( onPressed: () async { if (dontAskAgain) { await setting.put(SettingBoxKey.superResolutionWarn, true); } KazumiDialog.dismiss(); }, child: const Text('取消'), ), TextButton( onPressed: () async { confirmed = true; if (dontAskAgain) { await setting.put(SettingBoxKey.superResolutionWarn, true); } KazumiDialog.dismiss(); }, child: const Text('确认'), ), ], ); }); }); if (confirmed) { playerController.setShader(shaderIndex); } } else { playerController.setShader(shaderIndex); } } void handleFullscreen() { _handleFullscreenChange(context); if (videoPageController.isFullscreen) { Utils.exitFullScreen(); if (!Utils.isDesktop()) { widget.locateEpisode(); videoPageController.showTabBody = true; } } else { Utils.enterFullScreen(); videoPageController.showTabBody = false; } videoPageController.isFullscreen = !videoPageController.isFullscreen; } void displayVideoController() { animationController?.forward(); hideTimer?.cancel(); startHideTimer(); playerController.showVideoController = true; } void hideVideoController() { animationController?.reverse(); hideTimer?.cancel(); playerController.showVideoController = false; } Future setPlaybackSpeed(double speed) async { await playerController.setPlaybackSpeed(speed); } Future handleSpeedChange(String type) async { try { final currentSpeed = playerController.playerSpeed; int index = defaultPlaySpeedList.indexOf(currentSpeed); if (type == "up") { if (index < defaultPlaySpeedList.length - 1) { index++; setPlaybackSpeed(defaultPlaySpeedList[index]); } else { KazumiDialog.showToast(message: '已达倍速上限'); } } else if (type == "down") { if (index > 0) { index--; setPlaybackSpeed(defaultPlaySpeedList[index]); } else { KazumiDialog.showToast(message: '已达倍速下限'); } } } catch (e) { KazumiLogger().e('PlayerController: speed change failed', error: e); } } Future handleShortcutVolumeChange(String type) async { try { switch (type) { case 'up': await playerController.setVolume(playerController.volume + 10); break; case 'down': await playerController.setVolume(playerController.volume - 10); break; case 'mute': if (playerController.volume > 0) { lastVolume = playerController.volume; await playerController.setVolume(0); } else { await playerController.setVolume(lastVolume); } break; default: return; } playerController.showVolume = true; hideVolumeUITimer?.cancel(); hideVolumeUITimer = Timer(const Duration(seconds: 2), () { if (mounted) { playerController.showVolume = false; } hideVolumeUITimer = null; }); } catch (e) { KazumiLogger().e('PlayerController: volume change failed', error: e); } } Future setBrightness(double value) async { try { await ScreenBrightnessPlatform.instance .setApplicationScreenBrightness(value); } catch (_) {} } void startHideTimer() { hideTimer = Timer(const Duration(seconds: 4), () { if (mounted && playerController.canHidePlayerPanel) { playerController.showVideoController = false; animationController?.reverse(); } hideTimer = null; }); } // Used to pass hideTimer operation to panel layer void cancelHideTimer() { hideTimer?.cancel(); } Timer getPlayerTimer() { return Timer.periodic(const Duration(seconds: 1), (timer) { playerController.playing = playerController.playerPlaying; playerController.isBuffering = playerController.playerBuffering; playerController.currentPosition = playerController.playerPosition; playerController.buffer = playerController.playerBuffer; playerController.duration = playerController.playerDuration; playerController.completed = playerController.playerCompleted; // 弹幕相关 if (playerController.currentPosition.inMicroseconds != 0 && playerController.playerPlaying == true && playerController.danmakuOn == true) { playerController.danDanmakus[playerController.currentPosition.inSeconds] ?.asMap() .forEach((idx, danmaku) async { if (!_danmakuColor) { danmaku.color = Colors.white; } if (!_danmakuBiliBiliSource && danmaku.source.contains('BiliBili')) { return; } if (!_danmakuGamerSource && danmaku.source.contains('Gamer')) { return; } if (!_danmakuDanDanSource && !(danmaku.source.contains('BiliBili') || danmaku.source.contains('Gamer'))) { return; } await Future.delayed( Duration( milliseconds: idx * 1000 ~/ playerController .danDanmakus[ playerController.currentPosition.inSeconds]! .length), () => mounted && playerController.playerPlaying && !playerController.playerBuffering && playerController.danmakuOn && !myController.isDanmakuBlocked(danmaku.message) ? playerController.danmakuController.addDanmaku( DanmakuContentItem(danmaku.message, color: danmaku.color, type: danmaku.type == 4 ? DanmakuItemType.bottom : (danmaku.type == 5 ? DanmakuItemType.top : DanmakuItemType.scroll))) : null); }); } // 音量相关 if (!playerController.volumeSeeking) { if (Utils.isDesktop()) { playerController.volume = playerController.playerVolume; } else { FlutterVolumeController.getVolume().then((value) { final volume = value ?? 0.0; playerController.volume = volume * 100; }); } } // 亮度相关 if (!Platform.isWindows && !Platform.isMacOS && !Platform.isLinux && !playerController.brightnessSeeking) { ScreenBrightnessPlatform.instance.application.then((value) { playerController.brightness = value; }); } // 历史记录相关 if (playerController.playerPlaying && !videoPageController.loading && !videoPageController.isOfflineMode) { if (!WebDav().isHistorySyncing) { final pluginName = videoPageController.isOfflineMode ? videoPageController.offlinePluginName : videoPageController.currentPlugin.name; historyController.updateHistory( videoPageController.actualEpisodeNumber, videoPageController.currentRoad, pluginName, videoPageController.bangumiItem, playerController.playerPosition, videoPageController.src, videoPageController.roadList[videoPageController.currentRoad] .identifier[videoPageController.currentEpisode - 1]); } } // 自动播放下一集 if (playerController.completed && videoPageController.currentEpisode < videoPageController .roadList[videoPageController.currentRoad].data.length && !videoPageController.loading && autoPlayNext) { KazumiDialog.showToast( message: '正在加载${videoPageController.roadList[videoPageController.currentRoad].identifier[videoPageController.currentEpisode]}'); try { playerTimer!.cancel(); } catch (_) {} widget.changeEpisode(videoPageController.currentEpisode + 1, currentRoad: videoPageController.currentRoad); } // 一起去看相关 playerController.setSyncPlayCurrentPosition(); }); } void showDanmakuSearchDialog(String keyword) async { KazumiDialog.dismiss(); KazumiDialog.showLoading(msg: '弹幕检索中'); DanmakuSearchResponse danmakuSearchResponse; DanmakuEpisodeResponse danmakuEpisodeResponse; try { danmakuSearchResponse = await DanmakuRequest.getDanmakuSearchResponse(keyword); } catch (e) { KazumiDialog.dismiss(); KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}'); return; } KazumiDialog.dismiss(); if (danmakuSearchResponse.animes.isEmpty) { KazumiDialog.showToast(message: '未找到匹配结果'); return; } await KazumiDialog.show(builder: (context) { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: ListView( shrinkWrap: true, children: danmakuSearchResponse.animes.map((danmakuInfo) { return ListTile( title: Text(danmakuInfo.animeTitle), onTap: () async { KazumiDialog.dismiss(); KazumiDialog.showLoading(msg: '弹幕检索中'); try { danmakuEpisodeResponse = await DanmakuRequest.getDanDanEpisodesByDanDanBangumiID( danmakuInfo.animeId); } catch (e) { KazumiDialog.dismiss(); KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}'); return; } KazumiDialog.dismiss(); if (danmakuEpisodeResponse.episodes.isEmpty) { KazumiDialog.showToast(message: '未找到匹配结果'); return; } KazumiDialog.show(builder: (context) { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: ListView( shrinkWrap: true, children: danmakuEpisodeResponse.episodes.map((episode) { return ListTile( title: Text(episode.episodeTitle), onTap: () async { KazumiDialog.dismiss(); try { await playerController .getDanDanmakuByEpisodeID( episode.episodeId); KazumiDialog.showToast(message: '弹幕切换成功'); } catch (e) { KazumiDialog.showToast(message: '弹幕切换失败'); } }, ); }).toList(), ), ), ); }); }, ); }).toList(), ), ), ); }); } // 弹幕查询 void showDanmakuSwitch() { KazumiDialog.show( builder: (context) { final TextEditingController searchTextController = TextEditingController(); searchTextController.text = videoPageController.title; return AlertDialog( title: const Text('弹幕检索'), content: TextField( controller: searchTextController, decoration: const InputDecoration( hintText: '番剧名', ), onSubmitted: (keyword) { showDanmakuSearchDialog(keyword); }, ), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); widget.keyboardFocus.requestFocus(); }, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { showDanmakuSearchDialog(searchTextController.text); }, child: const Text( '提交', ), ), ], ); }, ); } Widget get videoInfoBody { return Observer(builder: (context) { return ListView( children: [ ListTile( title: const Text("Source"), subtitle: Text(playerController.videoUrl), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData(text: playerController.videoUrl), ); }, ), ListTile( title: const Text("Resolution"), subtitle: Text( '${playerController.playerWidth}x${playerController.playerHeight}'), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "Resolution\n${playerController.playerWidth}x${playerController.playerHeight}", ), ); }, ), ListTile( title: const Text("VideoParams"), subtitle: Text(playerController.playerVideoParams.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "VideoParams\n${playerController.playerVideoParams.toString()}", ), ); }, ), ListTile( title: const Text("AudioParams"), subtitle: Text(playerController.playerAudioParams.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "AudioParams\n${playerController.playerAudioParams.toString()}", ), ); }, ), ListTile( title: const Text("Media"), subtitle: Text(playerController.playerPlaylist.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "Media\n${playerController.playerPlaylist.toString()}", ), ); }, ), ListTile( title: const Text("AudioTrack"), subtitle: Text(playerController.playerAudioTracks.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "AudioTrack\n${playerController.playerAudioTracks.toString()}", ), ); }, ), ListTile( title: const Text("VideoTrack"), subtitle: Text(playerController.playerVideoTracks.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "VideoTrack\n${playerController.playerVideoTracks.toString()}", ), ); }, ), ListTile( title: const Text("AudioBitrate"), subtitle: Text(playerController.playerAudioBitrate.toString()), onTap: () { KazumiDialog.showToast(message: '已复制到剪贴板'); Clipboard.setData( ClipboardData( text: "AudioBitrate\n${playerController.playerAudioBitrate.toString()}", ), ); }, ), ], ); }); } Widget get videoDebugLogBody { return Scaffold( body: Padding( padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 0), child: Observer(builder: (context) { return ListView.builder( itemCount: playerController.playerLog.length, itemBuilder: (context, index) { return Text(playerController.playerLog[index]); }, ); }), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.copy), onPressed: () { Clipboard.setData( ClipboardData(text: playerController.playerLog.join('\n')), ); }), ); } void showVideoInfo() async { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return DefaultTabController( length: 2, child: Scaffold( body: Column( children: [ const PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Material( child: TabBar( tabs: [ Tab(text: '状态'), Tab(text: '日志'), ], ), ), ), Expanded( child: TabBarView( children: [ videoInfoBody, videoDebugLogBody, ], ), ), ], ), ), ); }); } void showSyncPlayEndPointSwitchDialog() { if (playerController.syncplayController != null) { KazumiDialog.showToast(message: 'SyncPlay: 请先退出当前房间再切换服务器'); return; } final String defaultCustomSyncPlayEndPoint = '自定义服务器'; String customSyncPlayEndPoint = defaultCustomSyncPlayEndPoint; String selectedSyncPlayEndPoint = setting.get( SettingBoxKey.syncPlayEndPoint, defaultValue: defaultSyncPlayEndPoint); KazumiDialog.show( builder: (context) { return StatefulBuilder(builder: (context, setDialogState) { List syncPlayEndPoints = []; syncPlayEndPoints.addAll(defaultSyncPlayEndPoints); syncPlayEndPoints.add(customSyncPlayEndPoint); if (!syncPlayEndPoints.contains(selectedSyncPlayEndPoint)) { syncPlayEndPoints.add(selectedSyncPlayEndPoint); } return AlertDialog( title: const Text('选择服务器'), content: SingleChildScrollView( child: ListBody( children: [ DropdownButtonFormField( decoration: InputDecoration( border: OutlineInputBorder(), ), isExpanded: true, value: selectedSyncPlayEndPoint, items: syncPlayEndPoints.map((String value) { return DropdownMenuItem( value: value, child: Text( value, overflow: TextOverflow.ellipsis, ), ); }).toList(), selectedItemBuilder: (context) { return syncPlayEndPoints.map((String value) { return Text( value, overflow: TextOverflow.ellipsis, ); }).toList(); }, onChanged: (String? newValue) { if (newValue != null) { if (newValue == defaultCustomSyncPlayEndPoint) { final serverTextController = TextEditingController(); KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('自定义服务器'), content: TextField( controller: serverTextController, decoration: const InputDecoration( hintText: '请输入服务器地址', ), ), actions: [ TextButton( child: const Text('取消'), onPressed: () { KazumiDialog.dismiss(); }, ), TextButton( child: const Text('确认'), onPressed: () { if (serverTextController .text.isNotEmpty && !syncPlayEndPoints.contains( serverTextController.text)) { KazumiDialog.dismiss(); setDialogState(() { customSyncPlayEndPoint = serverTextController.text; selectedSyncPlayEndPoint = serverTextController.text; }); } else { KazumiDialog.showToast( message: '服务器地址不能重复或为空'); } }, ), ], ); }, ); } else { setDialogState(() { selectedSyncPlayEndPoint = newValue; }); } } }, ), ], ), ), actions: [ TextButton( child: const Text('取消'), onPressed: () { KazumiDialog.dismiss(); }, ), TextButton( child: const Text('确认'), onPressed: () { setting.put( SettingBoxKey.syncPlayEndPoint, selectedSyncPlayEndPoint, ); KazumiDialog.dismiss(); }, ), ], ); }); }, ); } void showSyncPlayRoomCreateDialog() { final formKey = GlobalKey(); final TextEditingController roomController = TextEditingController(); final TextEditingController usernameController = TextEditingController(); KazumiDialog.show(builder: (BuildContext context) { return AlertDialog( title: const Text('加入房间'), content: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: roomController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '房间号', ), validator: (value) { if (value == null || value.isEmpty) { return '请输入房间号'; } final regex = RegExp(r'^[0-9]{6,10}$'); if (!regex.hasMatch(value)) { return '房间号需要6到10位数字'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: usernameController, decoration: const InputDecoration( labelText: '用户名', ), validator: (value) { if (value == null || value.isEmpty) { return '请输入用户名'; } final regex = RegExp(r'^[a-zA-Z]{4,12}$'); if (!regex.hasMatch(value)) { return '用户名必须为4到12位英文字符'; } return null; }, ), ], ), ), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: const Text('取消'), ), TextButton( onPressed: () { if (formKey.currentState!.validate()) { KazumiDialog.dismiss(); playerController.createSyncPlayRoom(roomController.text, usernameController.text, widget.changeEpisode); } }, child: const Text('确定'), ), ], ); }); } /// Used to decide which panel is used. /// It's too complicated to write these in conditional sentence. /// * true: use [PlayerItemPanel] /// * false: use [SmallestPlayerItemPanel] bool needFullPanel(BuildContext context) { // windows too small, workaround for ohos floating window if (MediaQuery.sizeOf(context).width < LayoutBreakpoint.compact['width']!) { return false; } // in desktop pip mode if (videoPageController.isPip) { return false; } // does not meet Google's phone landscape height and tablet landscape width requirements. if (!Utils.isDesktop() && (MediaQuery.sizeOf(context).height > LayoutBreakpoint.compact['height']! && MediaQuery.sizeOf(context).width < LayoutBreakpoint.medium['width']!)) { return false; } if (Utils.isDesktop() && (MediaQuery.sizeOf(context).height > LayoutBreakpoint.compact['height']! && MediaQuery.sizeOf(context).width < LayoutBreakpoint.compact['width']!)) { return false; } return true; } @override void onWindowRestore() { playerController.danmakuController.clear(); } @override void initState() { super.initState(); _loadShortcuts(); _initKeyboardActions(); _initPlayerMenu(); _fullscreenListener = mobx.reaction( (_) => videoPageController.isFullscreen, (_) { _handleFullscreenChange(context); }, ); // workaround for #214 if (Platform.isIOS) { FlutterVolumeController.setIOSAudioSessionCategory( category: AudioSessionCategory.playback); } WidgetsBinding.instance.addObserver(this); animationController ??= AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); webDavEnable = setting.get(SettingBoxKey.webDavEnable, defaultValue: false); webDavEnableHistory = setting.get(SettingBoxKey.webDavEnableHistory, defaultValue: false); playerController.danmakuOn = setting.get(SettingBoxKey.danmakuEnabledByDefault, defaultValue: false); _border = setting.get(SettingBoxKey.danmakuBorder, defaultValue: true); _opacity = setting.get(SettingBoxKey.danmakuOpacity, defaultValue: 1.0); _fontSize = setting.get(SettingBoxKey.danmakuFontSize, defaultValue: (Utils.isCompact()) ? 16.0 : 25.0); _danmakuArea = setting.get(SettingBoxKey.danmakuArea, defaultValue: 1.0); _hideTop = !setting.get(SettingBoxKey.danmakuTop, defaultValue: true); _hideBottom = !setting.get(SettingBoxKey.danmakuBottom, defaultValue: false); _hideScroll = !setting.get(SettingBoxKey.danmakuScroll, defaultValue: true); _massiveMode = setting.get(SettingBoxKey.danmakuMassive, defaultValue: false); _danmakuColor = setting.get(SettingBoxKey.danmakuColor, defaultValue: true); _danmakuDuration = setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0); _danmakuLineHeight = setting.get(SettingBoxKey.danmakuLineHeight, defaultValue: 1.6); _danmakuBiliBiliSource = setting.get(SettingBoxKey.danmakuBiliBiliSource, defaultValue: true); _danmakuGamerSource = setting.get(SettingBoxKey.danmakuGamerSource, defaultValue: true); _danmakuDanDanSource = setting.get(SettingBoxKey.danmakuDanDanSource, defaultValue: true); _danmakuFontWeight = setting.get(SettingBoxKey.danmakuFontWeight, defaultValue: 4); _danmakuUseSystemFont = setting.get(SettingBoxKey.useSystemFont, defaultValue: false); _danmakuBorderSize = setting.get(SettingBoxKey.danmakuBorderSize, defaultValue: 1.5); haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true); autoPlayNext = setting.get(SettingBoxKey.autoPlayNext, defaultValue: true); playerTimer = getPlayerTimer(); windowManager.addListener(this); displayVideoController(); } @override void dispose() { // Don't dispose player here // We need to reuse the player after episode is changed and player item is disposed // We dispose player after video page disposed _fullscreenListener(); WidgetsBinding.instance.removeObserver(this); windowManager.removeListener(this); playerTimer?.cancel(); hideTimer?.cancel(); mouseScrollerTimer?.cancel(); hideVolumeUITimer?.cancel(); animationController?.dispose(); animationController = null; _disposePlayerMenu(); // Reset player panel state playerController.lockPanel = false; playerController.showVideoController = true; playerController.showSeekTime = false; playerController.showBrightness = false; playerController.showVolume = false; playerController.showPlaySpeed = false; playerController.brightnessSeeking = false; playerController.volumeSeeking = false; playerController.canHidePlayerPanel = true; super.dispose(); } @override Widget build(BuildContext context) { collectType = collectController.getCollectType(videoPageController.bangumiItem); return Observer( builder: (context) { return ClipRect( child: Container( color: Colors.black, child: MouseRegion( cursor: (videoPageController.isFullscreen && !playerController.showVideoController) ? SystemMouseCursors.none : SystemMouseCursors.basic, onHover: (PointerEvent pointerEvent) { // workaround for android. // I don't know why, but android tap event will trigger onHover event. if (Utils.isDesktop()) { if (pointerEvent.position.dy > 50 && pointerEvent.position.dy < MediaQuery.of(context).size.height - 70) { _handleHove(); } else { if (!playerController.showVideoController) { animationController?.forward(); playerController.showVideoController = true; } } } }, child: Listener( onPointerSignal: (pointerSignal) { if (pointerSignal is PointerScrollEvent) { _handleMouseScroller(); final scrollDelta = pointerSignal.scrollDelta; final double volume = playerController.volume - scrollDelta.dy / 60; playerController.setVolume(volume); } }, child: SizedBox( height: videoPageController.isFullscreen ? (MediaQuery.of(context).size.height) : (MediaQuery.of(context).size.width * 9.0 / (16.0)), width: MediaQuery.of(context).size.width, child: Stack(alignment: Alignment.center, children: [ Center( child: Focus( // workaround for #461 // I don't know why, but the focus node will break popscope. focusNode: widget.keyboardFocus, autofocus: true, onKeyEvent: (focusNode, KeyEvent event) { bool handled = false; final keyLabel = event.logicalKey.keyLabel.isNotEmpty ? event.logicalKey.keyLabel : event.logicalKey.debugName ?? ''; if (event is KeyDownEvent) { handled = handleShortcutDown(keyLabel); } else if (event is KeyRepeatEvent) { handled = handleShortcutLongPress(keyLabel, "Repeat"); } else if (event is KeyUpEvent) { handled = handleShortcutLongPress(keyLabel, "Up"); } return handled ? KeyEventResult.handled : KeyEventResult.ignored; }, child: const PlayerItemSurface())), (playerController.isBuffering || videoPageController.loading) ? const Positioned.fill( child: Center( child: CircularProgressIndicator(), ), ) : Container(), GestureDetector( onTap: () { _handleTap(); }, onDoubleTap: (playerController.lockPanel) ? null : () { _handleDoubleTap(); }, onLongPressStart: (_) { if (playerController.lockPanel) { return; } setState(() { playerController.showPlaySpeed = true; }); lastPlayerSpeed = playerController.playerSpeed; setPlaybackSpeed(2.0); }, onLongPressEnd: (_) { if (playerController.lockPanel) { return; } setState(() { playerController.showPlaySpeed = false; }); setPlaybackSpeed(lastPlayerSpeed); }, child: Container( color: Colors.transparent, width: double.infinity, height: double.infinity, ), ), // 弹幕面板 Positioned( top: 0, left: 0, right: 0, height: videoPageController.isFullscreen ? MediaQuery.sizeOf(context).height : (MediaQuery.sizeOf(context).width * 9 / 16), child: DanmakuScreen( key: _danmuKey, createdController: (DanmakuController e) { playerController.danmakuController = e; WidgetsBinding.instance.addPostFrameCallback((_) { playerController.updateDanmakuSpeed(); }); }, option: DanmakuOption( hideTop: _hideTop, hideScroll: _hideScroll, hideBottom: _hideBottom, area: _danmakuArea, opacity: _opacity, fontSize: _fontSize, duration: _danmakuDuration / playerController.playerSpeed, lineHeight: _danmakuLineHeight, strokeWidth: _border ? _danmakuBorderSize : 0.0, fontWeight: _danmakuFontWeight, massiveMode: _massiveMode, fontFamily: _danmakuUseSystemFont ? null : customAppFontFamily, ), ), ), // 播放器控制面板 (needFullPanel(context)) ? PlayerItemPanel( onBackPressed: widget.onBackPressed, setPlaybackSpeed: setPlaybackSpeed, showDanmakuSwitch: showDanmakuSwitch, changeEpisode: widget.changeEpisode, openMenu: widget.openMenu, handleFullscreen: handleFullscreen, handleProgressBarDragStart: handleProgressBarDragStart, handleProgressBarDragEnd: handleProgressBarDragEnd, handleSuperResolutionChange: handleSuperResolutionChange, handlePreNextEpisode: handlePreNextEpisode, animationController: animationController!, keyboardFocus: widget.keyboardFocus, sendDanmaku: widget.sendDanmaku, startHideTimer: startHideTimer, cancelHideTimer: cancelHideTimer, handleDanmaku: handleDanmaku, showVideoInfo: showVideoInfo, showSyncPlayRoomCreateDialog: showSyncPlayRoomCreateDialog, showSyncPlayEndPointSwitchDialog: showSyncPlayEndPointSwitchDialog, showDanmakuDestinationPickerAndSend: widget.showDanmakuDestinationPickerAndSend, pauseForTimedShutdown: widget.pauseForTimedShutdown, disableAnimations: widget.disableAnimations, handleScreenShot: handleScreenshot, skipOP: skipOP, ) : SmallestPlayerItemPanel( onBackPressed: widget.onBackPressed, setPlaybackSpeed: setPlaybackSpeed, showDanmakuSwitch: showDanmakuSwitch, handleFullscreen: handleFullscreen, handleProgressBarDragStart: handleProgressBarDragStart, handleProgressBarDragEnd: handleProgressBarDragEnd, handleSuperResolutionChange: handleSuperResolutionChange, animationController: animationController!, keyboardFocus: widget.keyboardFocus, handleHove: _handleHove, startHideTimer: startHideTimer, cancelHideTimer: cancelHideTimer, handleDanmaku: handleDanmaku, showVideoInfo: showVideoInfo, showSyncPlayRoomCreateDialog: showSyncPlayRoomCreateDialog, showSyncPlayEndPointSwitchDialog: showSyncPlayEndPointSwitchDialog, pauseForTimedShutdown: widget.pauseForTimedShutdown, disableAnimations: widget.disableAnimations, skipOP: skipOP, ), // 播放器手势控制 Positioned.fill( left: 16, top: 25, right: 15, bottom: 15, child: (Utils.isDesktop() || playerController.lockPanel) ? Container() : GestureDetector( onHorizontalDragStart: (_) { if (!playerController.showVideoController) { animationController?.forward(); } playerController.canHidePlayerPanel = false; }, onHorizontalDragUpdate: (DragUpdateDetails details) { playerController.showSeekTime = true; playerTimer?.cancel(); playerController.pause(enableSync: false); final double scale = 180000 / MediaQuery.sizeOf(context).width; int ms = (playerController .currentPosition.inMilliseconds + (details.delta.dx * scale).round()) .clamp( 0, playerController .duration.inMilliseconds); playerController.currentPosition = Duration(milliseconds: ms); }, onHorizontalDragEnd: (_) { playerController.play(enableSync: false); playerController .seek(playerController.currentPosition); playerController.canHidePlayerPanel = true; if (!playerController.showVideoController) { animationController?.reverse(); } else { hideTimer?.cancel(); startHideTimer(); } playerTimer?.cancel(); playerTimer = getPlayerTimer(); playerController.showSeekTime = false; }, onVerticalDragUpdate: (DragUpdateDetails details) async { final double totalWidth = MediaQuery.sizeOf(context).width; final double totalHeight = MediaQuery.sizeOf(context).height; final double tapPosition = details.localPosition.dx; final double sectionWidth = totalWidth / 2; final double delta = details.delta.dy; if (tapPosition < sectionWidth) { // 左边区域 playerController.brightnessSeeking = true; playerController.showBrightness = true; final double level = (totalHeight) * 2; final double brightness = playerController.brightness - delta / level; final double result = brightness.clamp(0.0, 1.0); setBrightness(result); playerController.brightness = result; } else { // 右边区域 playerController.volumeSeeking = true; playerController.showVolume = true; final double level = (totalHeight) * 0.03; final double volume = playerController.volume - delta / level; playerController.setVolume(volume); } }, onVerticalDragEnd: (_) { if (playerController.volumeSeeking) { playerController.volumeSeeking = false; Future.delayed(const Duration(seconds: 1), () { FlutterVolumeController.updateShowSystemUI( true); }); } if (playerController.brightnessSeeking) { playerController.brightnessSeeking = false; } playerController.showVolume = false; playerController.showBrightness = false; }, ), ), ]), ), ), ), ), ); }, ); } } ================================================ FILE: lib/pages/player/player_item_panel.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/player/player_controller.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/utils/remote.dart'; import 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb; import 'package:kazumi/pages/settings/danmaku/danmaku_settings_sheet.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:kazumi/utils/timed_shutdown_service.dart'; import 'package:kazumi/pages/download/download_controller.dart'; class PlayerItemPanel extends StatefulWidget { const PlayerItemPanel({ super.key, required this.onBackPressed, required this.setPlaybackSpeed, required this.showDanmakuSwitch, required this.changeEpisode, required this.handleFullscreen, required this.handleScreenShot, required this.handlePreNextEpisode, required this.handleProgressBarDragStart, required this.handleProgressBarDragEnd, required this.handleSuperResolutionChange, required this.animationController, required this.openMenu, required this.keyboardFocus, required this.sendDanmaku, required this.startHideTimer, required this.cancelHideTimer, required this.handleDanmaku, required this.skipOP, required this.showVideoInfo, required this.showSyncPlayRoomCreateDialog, required this.showSyncPlayEndPointSwitchDialog, required this.showDanmakuDestinationPickerAndSend, required this.pauseForTimedShutdown, this.disableAnimations = false, }); final void Function(BuildContext) onBackPressed; final Future Function(double) setPlaybackSpeed; final void Function() showDanmakuSwitch; final Future Function(int, {int currentRoad, int offset}) changeEpisode; final void Function() openMenu; final void Function() handleFullscreen; final void Function() handleScreenShot; final void Function(ThumbDragDetails details) handleProgressBarDragStart; final void Function() handleProgressBarDragEnd; final Future Function(int shaderIndex) handleSuperResolutionChange; final AnimationController animationController; final FocusNode keyboardFocus; final void Function() startHideTimer; final void Function() cancelHideTimer; final void Function() handleDanmaku; final void Function(String direction) handlePreNextEpisode; final void Function() skipOP; final void Function(String) sendDanmaku; final void Function() showVideoInfo; final void Function() showSyncPlayRoomCreateDialog; final void Function() showSyncPlayEndPointSwitchDialog; final void Function(String) showDanmakuDestinationPickerAndSend; final VoidCallback pauseForTimedShutdown; final bool disableAnimations; @override State createState() => _PlayerItemPanelState(); } class _PlayerItemPanelState extends State { Box setting = GStorage.setting; late bool haEnable; late Animation topOffsetAnimation; late Animation bottomOffsetAnimation; late Animation leftOffsetAnimation; final VideoPageController videoPageController = Modular.get(); final PlayerController playerController = Modular.get(); final DownloadController downloadController = Modular.get(); final TextEditingController textController = TextEditingController(); final FocusNode textFieldFocus = FocusNode(); // SVG Caches String? cachedSvgString; Widget? cachedDanmakuOnIcon; Widget? cachedDanmakuOffIcon; Widget? cachedDanmakuSettingIcon; static const double _danmakuIconSize = 24.0; static const double _loadingIndicatorStrokeWidth = 2.0; Widget get danmakuTextField { return Container( constraints: Utils.isDesktop() ? const BoxConstraints(maxWidth: 500, maxHeight: 33) : const BoxConstraints(maxHeight: 33), padding: const EdgeInsets.symmetric(horizontal: 8), child: TextField( focusNode: textFieldFocus, style: TextStyle( fontSize: Utils.isDesktop() ? 15 : 13, color: Colors.white), controller: textController, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( enabled: playerController.danmakuOn, filled: true, fillColor: Colors.white38, floatingLabelBehavior: FloatingLabelBehavior.never, hintText: playerController.danmakuOn ? '发个友善的弹幕见证当下' : '已关闭弹幕', hintStyle: TextStyle( fontSize: Utils.isDesktop() ? 15 : 13, color: Colors.white60), alignLabelWithHint: true, contentPadding: EdgeInsets.symmetric( vertical: 8, horizontal: Utils.isDesktop() ? 8 : 12), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(Utils.isDesktop() ? 8 : 20)), ), suffixIconConstraints: const BoxConstraints(minWidth: 0), suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { textFieldFocus.unfocus(); widget.showDanmakuDestinationPickerAndSend(textController.text); textController.clear(); }, style: TextButton.styleFrom( foregroundColor: playerController.danmakuOn ? Theme.of(context).colorScheme.onPrimaryContainer : Colors.white60, backgroundColor: playerController.danmakuOn ? Theme.of(context).colorScheme.primaryContainer : Theme.of(context).disabledColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Utils.isDesktop() ? 8 : 20), ), ), child: const Text('发送'), ), ], ), ), onTapAlwaysCalled: true, onTap: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onSubmitted: (msg) { textFieldFocus.unfocus(); widget.showDanmakuDestinationPickerAndSend(msg); widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; textController.clear(); }, onTapOutside: (_) { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; textFieldFocus.unfocus(); widget.keyboardFocus.requestFocus(); }, ), ); } // 选择倍速 void showSetSpeedSheet() { final double currentSpeed = playerController.playerSpeed; KazumiDialog.show(builder: (context) { return AlertDialog( title: const Text('播放速度'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Wrap( spacing: 8, runSpacing: Utils.isDesktop() ? 8 : 0, children: [ for (final double i in defaultPlaySpeedList) ...[ if (i == currentSpeed) FilledButton( onPressed: () async { await widget.setPlaybackSpeed(i); KazumiDialog.dismiss(); }, child: Text(i.toString()), ) else FilledButton.tonal( onPressed: () async { await widget.setPlaybackSpeed(i); KazumiDialog.dismiss(); }, child: Text(i.toString()), ), ] ], ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { await widget.setPlaybackSpeed(1.0); KazumiDialog.dismiss(); }, child: const Text('默认速度'), ), ], ); }); } void showForwardChange() { KazumiDialog.show(builder: (context) { String input = ""; return AlertDialog( title: const Text('跳过秒数'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // 只允许输入数字 ], decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, // 控制label的显示方式 labelText: playerController.buttonSkipTime.toString(), ), onChanged: (value) { input = value; }, ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { if (input != "") { playerController.setButtonForwardTime(int.parse(input)); KazumiDialog.dismiss(); } else { KazumiDialog.dismiss(); } }, child: const Text('确定'), ), ], ); }); } @override void initState() { super.initState(); topOffsetAnimation = Tween( begin: const Offset(0.0, -1.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); bottomOffsetAnimation = Tween( begin: const Offset(0.0, 1.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); leftOffsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true); cacheSvgIcons(); } void cacheSvgIcons() { cachedDanmakuOffIcon = RepaintBoundary( child: SvgPicture.asset( 'assets/images/danmaku_off.svg', height: _danmakuIconSize, ), ); cachedDanmakuSettingIcon = RepaintBoundary( child: SvgPicture.asset( 'assets/images/danmaku_setting.svg', height: _danmakuIconSize, ), ); } Widget danmakuOnIcon(BuildContext context) { final colorHex = Theme.of(context) .colorScheme .primary .toARGB32() .toRadixString(16) .substring(2); if (cachedSvgString != colorHex) { cachedSvgString = colorHex; final svgString = danmakuOnSvg.replaceFirst('00AEEC', colorHex); cachedDanmakuOnIcon = RepaintBoundary( child: SvgPicture.string( svgString, height: _danmakuIconSize, ), ); } return cachedDanmakuOnIcon!; } Widget _buildDanmakuToggleButton(BuildContext context) { return IconButton( color: Colors.white, icon: playerController.danmakuLoading ? SizedBox( width: _danmakuIconSize, height: _danmakuIconSize, child: CircularProgressIndicator( strokeWidth: _loadingIndicatorStrokeWidth, ), ) : (playerController.danmakuOn ? danmakuOnIcon(context) : cachedDanmakuOffIcon!), onPressed: playerController.danmakuLoading ? null : () { widget.handleDanmaku(); }, tooltip: playerController.danmakuLoading ? '弹幕加载中...' : (playerController.danmakuOn ? '关闭弹幕' : '打开弹幕'), ); } Widget forwardIcon() { return Tooltip( message: '长按修改时间', child: GestureDetector( onLongPress: () => showForwardChange(), child: IconButton( icon: Image.asset( 'assets/images/forward_80.png', color: Colors.white, height: 24, ), onPressed: () { widget.skipOP(); }, ), ), ); } @override Widget build(BuildContext context) { return Observer(builder: (context) { return Stack( alignment: Alignment.center, children: [ //顶部渐变区域 AnimatedPositioned( duration: const Duration(seconds: 1), top: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? Container( height: 50, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black45, Colors.transparent, ], ), ), ) : SlideTransition( position: topOffsetAnimation, child: Container( height: 50, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black45, Colors.transparent, ], ), ), ), ), ), ), //底部渐变区域 AnimatedPositioned( duration: const Duration(seconds: 1), bottom: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? Container( height: 100, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black45, ], ), ), ) : SlideTransition( position: bottomOffsetAnimation, child: Container( height: 100, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black45, ], ), ), ), ), ), ), // 顶部进度条 Positioned( top: 25, child: playerController.showSeekTime ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Text( playerController.currentPosition.compareTo( playerController.playerPosition) > 0 ? '快进 ${playerController.currentPosition.inSeconds - playerController.playerPosition.inSeconds} 秒' : '快退 ${playerController.playerPosition.inSeconds - playerController.currentPosition.inSeconds} 秒', style: const TextStyle( color: Colors.white, ), ), ), ], ) : Container()), // 顶部播放速度条 Positioned( top: 25, child: playerController.showPlaySpeed ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: const Row( children: [ Icon(Icons.fast_forward, color: Colors.white), Text( ' 倍速播放', style: TextStyle( color: Colors.white, ), ), ], ), ), ], ) : Container()), // 亮度条 Positioned( top: 25, child: playerController.showBrightness ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Row( children: [ const Icon(Icons.brightness_7, color: Colors.white), Text( ' ${(playerController.brightness * 100).toInt()} %', style: const TextStyle( color: Colors.white, ), ), ], )), ], ) : Container()), // 音量条 Positioned( top: 25, child: playerController.showVolume ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Row( children: [ const Icon(Icons.volume_down, color: Colors.white), Text( ' ${playerController.volume.toInt()}%', style: const TextStyle( color: Colors.white, ), ), ], )), ], ) : Container()), // 右侧锁定按钮 (Utils.isDesktop() || !videoPageController.isFullscreen) ? Container() : Positioned( right: 0, top: 0, bottom: 0, child: Visibility( visible: widget.disableAnimations ? playerController.showVideoController : true, child: widget.disableAnimations ? leftControlWidget : SlideTransition( position: leftOffsetAnimation, child: leftControlWidget), ), ), // 自定义顶部组件 Positioned( top: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? topControlWidget : SlideTransition( position: topOffsetAnimation, child: topControlWidget), ), ), // 自定义播放器底部组件 Positioned( bottom: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? bottomControlWidget : SlideTransition( position: bottomOffsetAnimation, child: bottomControlWidget), ), ), ], ); }); } Widget get bottomControlWidget { return Observer( builder: (context) { return SafeArea( top: false, bottom: videoPageController.isFullscreen, left: videoPageController.isFullscreen, right: videoPageController.isFullscreen, child: MouseRegion( cursor: (videoPageController.isFullscreen && !playerController.showVideoController) ? SystemMouseCursors.none : SystemMouseCursors.basic, onEnter: (_) { widget.cancelHideTimer(); }, onExit: (_) { widget.cancelHideTimer(); widget.startHideTimer(); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!Utils.isDesktop() && !Utils.isTablet()) Container( padding: const EdgeInsets.only(left: 10.0, bottom: 10), child: Text( "${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}", style: const TextStyle( color: Colors.white, fontSize: 12.0, fontFeatures: [ FontFeature.tabularFigures(), ], ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: ProgressBar( thumbRadius: 8, thumbGlowRadius: 18, timeLabelLocation: Utils.isTablet() ? TimeLabelLocation.sides : TimeLabelLocation.none, timeLabelTextStyle: const TextStyle( color: Colors.white, fontSize: 12.0, fontFeatures: [ FontFeature.tabularFigures(), ], ), progress: playerController.currentPosition, buffered: playerController.buffer, total: playerController.duration, onSeek: (duration) { playerController.seek(duration); }, onDragStart: (details) { widget.handleProgressBarDragStart(details); }, onDragUpdate: (details) => {playerController.currentPosition = details.timeStamp}, onDragEnd: () { widget.handleProgressBarDragEnd(); }, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ IconButton( color: Colors.white, icon: Icon(playerController.playing ? Icons.pause_rounded : Icons.play_arrow_rounded), onPressed: () { playerController.playOrPause(); }, ), // 更换选集 if (videoPageController.isFullscreen || Utils.isTablet() || Utils.isDesktop()) IconButton( color: Colors.white, icon: const Icon(Icons.skip_next_rounded), onPressed: () => widget.handlePreNextEpisode('next'), ), if (Utils.isDesktop()) Container( padding: const EdgeInsets.only(left: 10.0), child: Text( "${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}", style: const TextStyle( color: Colors.white, fontSize: 16.0, fontFeatures: [ FontFeature.tabularFigures(), ], ), ), ), if (Utils.isDesktop()) Expanded( child: LayoutBuilder( builder: (context, constraints) { bool isSpaceEnough = constraints.maxWidth > 600; return Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildDanmakuToggleButton(context), IconButton( onPressed: () { widget.keyboardFocus.requestFocus(); showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context) .size .height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context) .size .width * 9 / 16 : MediaQuery.of(context) .size .width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return DanmakuSettingsSheet( danmakuController: playerController .danmakuController, onUpdateDanmakuSpeed: playerController.updateDanmakuSpeed, ); }); }, color: Colors.white, icon: cachedDanmakuSettingIcon!, ), if (isSpaceEnough) danmakuTextField, ], ), ); }, ), ), if (!Utils.isDesktop()) ...[ IconButton( color: Colors.white, icon: playerController.danmakuOn ? danmakuOnIcon(context) : cachedDanmakuOffIcon!, onPressed: () { widget.handleDanmaku(); }, tooltip: playerController.danmakuOn ? '关闭弹幕' : '打开弹幕', ), if (playerController.danmakuOn) ...[ IconButton( onPressed: () { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return DanmakuSettingsSheet( danmakuController: playerController.danmakuController, onUpdateDanmakuSpeed: playerController.updateDanmakuSpeed, ); }); }, color: Colors.white, icon: cachedDanmakuSettingIcon!, ), Expanded(child: danmakuTextField), ], if (!playerController.danmakuOn) const Spacer(), ], // 超分辨率 MenuAnchor( consumeOutsideTap: true, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, builder: (BuildContext context, MenuController controller, Widget? child) { return TextButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, child: const Text( '超分辨率', style: TextStyle(color: Colors.white), ), ); }, menuChildren: List.generate( 3, (int index) => MenuItemButton( onPressed: () => widget.handleSuperResolutionChange(index + 1), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( index + 1 == 1 ? '关闭' : index + 1 == 2 ? '效率档' : '质量档', style: TextStyle( color: playerController.superResolutionType == index + 1 ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ), ), // 倍速播放 MenuAnchor( consumeOutsideTap: true, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, builder: (BuildContext context, MenuController controller, Widget? child) { return TextButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, child: Text( playerController.playerSpeed == 1.0 ? '倍速' : '${playerController.playerSpeed}x', style: const TextStyle(color: Colors.white), ), ); }, menuChildren: [ for (final double i in defaultPlaySpeedList) ...[ MenuItemButton( onPressed: () async { await widget.setPlaybackSpeed(i); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( '${i}x', style: TextStyle( color: i == playerController.playerSpeed ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ], ], ), MenuAnchor( consumeOutsideTap: true, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, builder: (BuildContext context, MenuController controller, Widget? child) { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: const Icon( Icons.aspect_ratio_rounded, color: Colors.white, ), tooltip: '视频比例', ); }, menuChildren: [ for (final entry in aspectRatioTypeMap.entries) MenuItemButton( onPressed: () => playerController.aspectRatioType = entry.key, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( entry.value, style: TextStyle( color: entry.key == playerController.aspectRatioType ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ], ), (!videoPageController.isFullscreen && !Utils.isTablet() && !Utils.isDesktop()) ? Container() : IconButton( color: Colors.white, icon: const Icon(Icons.menu_open_rounded), onPressed: () { videoPageController.showTabBody = !videoPageController.showTabBody; widget.openMenu(); }, ), (Utils.isTablet() && videoPageController.isFullscreen && MediaQuery.of(context).size.height < MediaQuery.of(context).size.width) ? Container() : IconButton( color: Colors.white, icon: Icon(videoPageController.isFullscreen ? Icons.fullscreen_exit_rounded : Icons.fullscreen_rounded), onPressed: () { widget.handleFullscreen(); }, ), ], ), ), if (Utils.isTablet() || Utils.isDesktop()) const SizedBox(height: 6), ], ), ), ); } ); } Widget get topControlWidget { return Observer( builder: (context) { return EmbeddedNativeControlArea( requireOffset: !videoPageController.isFullscreen, child: SafeArea( top: false, bottom: false, left: videoPageController.isFullscreen, right: videoPageController.isFullscreen, child: MouseRegion( cursor: (videoPageController.isFullscreen && !playerController.showVideoController) ? SystemMouseCursors.none : SystemMouseCursors.basic, onEnter: (_) { widget.cancelHideTimer(); }, onExit: (_) { widget.cancelHideTimer(); widget.startHideTimer(); }, child: Row( children: [ IconButton( color: Colors.white, icon: const Icon(Icons.arrow_back_rounded), onPressed: () { widget.onBackPressed(context); }, ), // 拖动条 Expanded( child: dtb.DragToMoveArea( child: Text( ' ${videoPageController.title} [${videoPageController.roadList[videoPageController.currentRoad].identifier[videoPageController.currentEpisode - 1]}]', style: TextStyle( color: Colors.white, fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, overflow: TextOverflow.ellipsis, ), ), ), ), // 跳过 forwardIcon(), if (Utils.isDesktop() && !videoPageController.isFullscreen) IconButton( onPressed: () { if (videoPageController.isPip) { Utils.exitDesktopPIPWindow(); } else { Utils.enterDesktopPIPWindow(); } videoPageController.isPip = !videoPageController.isPip; }, icon: const Icon( Icons.picture_in_picture, color: Colors.white, ), ), // 追番 CollectButton( bangumiItem: videoPageController.bangumiItem, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, ), MenuAnchor( consumeOutsideTap: true, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, builder: (BuildContext context, MenuController controller, Widget? child) { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: const Icon( Icons.more_vert, color: Colors.white, ), ); }, menuChildren: [ MenuItemButton( onPressed: () { widget.showDanmakuSwitch(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("弹幕切换"), ), ), ), MenuItemButton( onPressed: () { widget.showVideoInfo(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("视频详情"), ), ), ), MenuItemButton( onPressed: () { bool needRestart = playerController.playing; playerController.pause(); RemotePlay() .castVideo(playerController.videoUrl, videoPageController.currentPlugin.referer) .whenComplete(() { if (needRestart) { playerController.play(); } }); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("远程投屏"), ), ), ), MenuItemButton( onPressed: () { playerController.lanunchExternalPlayer(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("外部播放"), ), ), ), // 定时关闭 SubmenuButton( menuChildren: [ MenuItemButton( onPressed: () { TimedShutdownService().cancel(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "不开启", style: TextStyle( color: !TimedShutdownService().isActive ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), for (final int minutes in [15, 30, 60]) MenuItemButton( onPressed: () { TimedShutdownService().start(minutes, onExpired: widget.pauseForTimedShutdown); KazumiDialog.showToast(message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭'); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "$minutes 分钟", style: TextStyle( color: TimedShutdownService().setMinutes == minutes ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), MenuItemButton( onPressed: () { TimedShutdownService.showCustomTimerDialog( onExpired: widget.pauseForTimedShutdown, ); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("自定义"), ), ), ), ], child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: ValueListenableBuilder( valueListenable: TimedShutdownService().remainingSecondsNotifier, builder: (context, remainingSeconds, child) { return Text( remainingSeconds > 0 ? "定时关闭 (${TimedShutdownService().formatRemainingTime()})" : "定时关闭", ); }, ), ), ), ), SubmenuButton( menuChildren: [ MenuItemButton( child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "当前房间: ${playerController.syncplayRoom == '' ? '未加入' : playerController.syncplayRoom}"), ), ), ), MenuItemButton( child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "网络延时: ${playerController.syncplayClientRtt}ms"), ), ), ), MenuItemButton( onPressed: () { widget.showSyncPlayRoomCreateDialog(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("加入房间"), ), ), ), MenuItemButton( onPressed: () { widget.showSyncPlayEndPointSwitchDialog(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("切换服务器"), ), ), ), MenuItemButton( onPressed: () async { await playerController.exitSyncPlayRoom(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("断开连接"), ), ), ), ], child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("一起看"), ), ), ), ], ), ], ), ), ), ); } ); } Widget get leftControlWidget { return Observer( builder: (context) { return SafeArea( top: false, bottom: false, left: videoPageController.isFullscreen, right: videoPageController.isFullscreen, child: Column( children: [ const Spacer(), (playerController.lockPanel) ? Container() : IconButton( icon: const Icon( Icons.photo_camera_outlined, color: Colors.white, ), onPressed: () { widget.handleScreenShot(); }, ), IconButton( icon: Icon( playerController.lockPanel ? Icons.lock_outline : Icons.lock_open, color: Colors.white, ), onPressed: () { playerController.lockPanel = !playerController.lockPanel; }, ), const Spacer(), ], ), ); } ); } } ================================================ FILE: lib/pages/player/player_item_surface.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/player/player_controller.dart'; class PlayerItemSurface extends StatefulWidget { const PlayerItemSurface({super.key}); @override State createState() => _PlayerItemSurfaceState(); } class _PlayerItemSurfaceState extends State { final PlayerController playerController = Modular.get(); @override Widget build(BuildContext context) { return Observer(builder: (context) { if (playerController.loading || playerController.videoController == null) { return Container( color: Colors.black, child: const Center( child: CircularProgressIndicator(), ), ); } return Video( controller: playerController.videoController!, controls: NoVideoControls, fit: playerController.aspectRatioType == 1 ? BoxFit.contain : playerController.aspectRatioType == 2 ? BoxFit.cover : BoxFit.fill, subtitleViewConfiguration: SubtitleViewConfiguration( style: TextStyle( color: Colors.pink, fontSize: 48.0, background: Paint()..color = Colors.transparent, decoration: TextDecoration.none, fontWeight: FontWeight.bold, shadows: const [ Shadow( offset: Offset(1.0, 1.0), blurRadius: 3.0, color: Color.fromARGB(255, 255, 255, 255), ), Shadow( offset: Offset(-1.0, -1.0), blurRadius: 3.0, color: Color.fromARGB(125, 255, 255, 255), ), ], ), textAlign: TextAlign.center, padding: const EdgeInsets.all(24.0), ), ); }); } } ================================================ FILE: lib/pages/player/smallest_player_item_panel.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/player/player_controller.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/utils/remote.dart'; import 'package:kazumi/pages/settings/danmaku/danmaku_settings_sheet.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/utils/timed_shutdown_service.dart'; class SmallestPlayerItemPanel extends StatefulWidget { const SmallestPlayerItemPanel({ super.key, required this.onBackPressed, required this.setPlaybackSpeed, required this.showDanmakuSwitch, required this.handleFullscreen, required this.handleProgressBarDragStart, required this.handleProgressBarDragEnd, required this.handleSuperResolutionChange, required this.animationController, required this.keyboardFocus, required this.handleHove, required this.startHideTimer, required this.cancelHideTimer, required this.handleDanmaku, required this.skipOP, required this.showVideoInfo, required this.showSyncPlayRoomCreateDialog, required this.showSyncPlayEndPointSwitchDialog, required this.pauseForTimedShutdown, this.disableAnimations = false, }); final void Function(BuildContext) onBackPressed; final Future Function(double) setPlaybackSpeed; final void Function() showDanmakuSwitch; final void Function() handleDanmaku; final void Function() skipOP; final void Function() handleFullscreen; final void Function(ThumbDragDetails details) handleProgressBarDragStart; final void Function() handleProgressBarDragEnd; final Future Function(int shaderIndex) handleSuperResolutionChange; final void Function() handleHove; final AnimationController animationController; final FocusNode keyboardFocus; final void Function() startHideTimer; final void Function() cancelHideTimer; final void Function() showVideoInfo; final void Function() showSyncPlayRoomCreateDialog; final void Function() showSyncPlayEndPointSwitchDialog; final VoidCallback pauseForTimedShutdown; final bool disableAnimations; @override State createState() => _SmallestPlayerItemPanelState(); } class _SmallestPlayerItemPanelState extends State { Box setting = GStorage.setting; late bool haEnable; late Animation topOffsetAnimation; late Animation bottomOffsetAnimation; late Animation leftOffsetAnimation; final VideoPageController videoPageController = Modular.get(); final PlayerController playerController = Modular.get(); final TextEditingController textController = TextEditingController(); // SVG Caches String? cachedSvgString; Widget? cachedDanmakuOnIcon; Widget? cachedDanmakuOffIcon; static const double _danmakuIconSize = 24.0; static const double _loadingIndicatorStrokeWidth = 2.0; void showForwardChange() { KazumiDialog.show(builder: (context) { String input = ""; return AlertDialog( title: const Text('跳过秒数'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // 只允许输入数字 ], decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, // 控制label的显示方式 labelText: playerController.buttonSkipTime.toString(), ), onChanged: (value) { input = value; }, ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { if (input != "") { playerController.setButtonForwardTime(int.parse(input)); KazumiDialog.dismiss(); } else { KazumiDialog.dismiss(); } }, child: const Text('确定'), ), ], ); }); } @override void initState() { super.initState(); topOffsetAnimation = Tween( begin: const Offset(0.0, -1.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); bottomOffsetAnimation = Tween( begin: const Offset(0.0, 1.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); leftOffsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: widget.animationController, curve: Curves.easeInOut, )); haEnable = setting.get(SettingBoxKey.hAenable, defaultValue: true); cacheSvgIcons(); } void cacheSvgIcons() { cachedDanmakuOffIcon = RepaintBoundary( child: SvgPicture.asset( 'assets/images/danmaku_off.svg', height: _danmakuIconSize, ), ); } Widget danmakuOnIcon(BuildContext context) { final colorHex = Theme.of(context) .colorScheme .primary .toARGB32() .toRadixString(16) .substring(2); if (cachedSvgString != colorHex) { cachedSvgString = colorHex; final svgString = danmakuOnSvg.replaceFirst('00AEEC', colorHex); cachedDanmakuOnIcon = RepaintBoundary( child: SvgPicture.string( svgString, height: _danmakuIconSize, ), ); } return cachedDanmakuOnIcon!; } Widget _buildDanmakuToggleButton(BuildContext context) { return IconButton( color: Colors.white, icon: playerController.danmakuLoading ? SizedBox( width: _danmakuIconSize, height: _danmakuIconSize, child: CircularProgressIndicator( strokeWidth: _loadingIndicatorStrokeWidth, ), ) : (playerController.danmakuOn ? danmakuOnIcon(context) : cachedDanmakuOffIcon!), onPressed: playerController.danmakuLoading ? null : () { widget.handleDanmaku(); }, tooltip: playerController.danmakuLoading ? '弹幕加载中...' : (playerController.danmakuOn ? '关闭弹幕' : '打开弹幕'), ); } Widget forwardIcon() { return Tooltip( message: '长按修改时间', child: GestureDetector( onLongPress: () => showForwardChange(), child: IconButton( icon: Image.asset( 'assets/images/forward_80.png', color: Colors.white, height: 24, ), onPressed: () { widget.skipOP(); }, ), ), ); } @override Widget build(BuildContext context) { return Observer(builder: (context) { return Stack( alignment: Alignment.center, children: [ //顶部渐变区域 AnimatedPositioned( duration: const Duration(seconds: 1), top: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? Container( height: 50, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black45, Colors.transparent, ], ), ), ) : SlideTransition( position: topOffsetAnimation, child: Container( height: 50, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black45, Colors.transparent, ], ), ), ), ), ), ), //底部渐变区域 AnimatedPositioned( duration: const Duration(seconds: 1), bottom: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? Container( height: 100, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black45, ], ), ), ) : SlideTransition( position: bottomOffsetAnimation, child: Container( height: 100, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black45, ], ), ), ), ), ), ), // 顶部进度条 Positioned( top: 25, child: playerController.showSeekTime ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Text( playerController.currentPosition.compareTo( playerController.playerPosition) > 0 ? '快进 ${playerController.currentPosition.inSeconds - playerController.playerPosition.inSeconds} 秒' : '快退 ${playerController.playerPosition.inSeconds - playerController.currentPosition.inSeconds} 秒', style: const TextStyle( color: Colors.white, ), ), ), ], ) : Container()), // 顶部播放速度条 Positioned( top: 25, child: playerController.showPlaySpeed ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: const Row( children: [ Icon(Icons.fast_forward, color: Colors.white), Text( ' 倍速播放', style: TextStyle( color: Colors.white, ), ), ], ), ), ], ) : Container()), // 亮度条 Positioned( top: 25, child: playerController.showBrightness ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Row( children: [ const Icon(Icons.brightness_7, color: Colors.white), Text( ' ${(playerController.brightness * 100).toInt()} %', style: const TextStyle( color: Colors.white, ), ), ], )), ], ) : Container()), // 音量条 Positioned( top: 25, child: playerController.showVolume ? Wrap( alignment: WrapAlignment.center, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8.0), // 圆角 ), child: Row( children: [ const Icon(Icons.volume_down, color: Colors.white), Text( ' ${playerController.volume.toInt()}%', style: const TextStyle( color: Colors.white, ), ), ], )), ], ) : Container()), // 自定义顶部组件 Positioned( top: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? topControlWidget : SlideTransition( position: topOffsetAnimation, child: topControlWidget), ), ), // 自定义播放器底部组件 Positioned( bottom: 0, left: 0, right: 0, child: Visibility( visible: !playerController.lockPanel && (widget.disableAnimations ? playerController.showVideoController : true), child: widget.disableAnimations ? bottomControlWidget : SlideTransition( position: bottomOffsetAnimation, child: bottomControlWidget), ), ), ], ); }); } Widget get bottomControlWidget { return Observer(builder: (context) { return Row( children: [ IconButton( color: Colors.white, icon: Icon(playerController.playing ? Icons.pause_rounded : Icons.play_arrow_rounded), onPressed: () { playerController.playOrPause(); }, ), Expanded( child: ProgressBar( thumbRadius: 8, thumbGlowRadius: 18, timeLabelLocation: TimeLabelLocation.none, progress: playerController.currentPosition, buffered: playerController.buffer, total: playerController.duration, onSeek: (duration) { playerController.seek(duration); }, onDragStart: (details) { widget.handleProgressBarDragStart(details); }, onDragUpdate: (details) => {playerController.currentPosition = details.timeStamp}, onDragEnd: () { widget.handleProgressBarDragEnd(); }, ), ), Text( " ${Utils.durationToString(playerController.currentPosition)} / ${Utils.durationToString(playerController.duration)}", style: const TextStyle( color: Colors.white, fontSize: 12.0, fontFeatures: [ FontFeature.tabularFigures(), ], ), ), (!videoPageController.isPip) ? IconButton( color: Colors.white, icon: Icon(videoPageController.isFullscreen ? Icons.fullscreen_exit_rounded : Icons.fullscreen_rounded), onPressed: () { widget.handleFullscreen(); }, ) : const Text(' '), ], ); }); } Widget get topControlWidget { return Observer(builder: (context) { return EmbeddedNativeControlArea( child: Row( children: [ IconButton( color: Colors.white, icon: const Icon(Icons.arrow_back_rounded), onPressed: () { widget.onBackPressed(context); }, ), // 拖动条 const Expanded( child: dtb.DragToMoveArea(child: SizedBox(height: 40)), ), // 跳过 forwardIcon(), if (Utils.isDesktop()) IconButton( onPressed: () { if (videoPageController.isPip) { Utils.exitDesktopPIPWindow(); } else { Utils.enterDesktopPIPWindow(); } videoPageController.isPip = !videoPageController.isPip; }, icon: const Icon(Icons.picture_in_picture, color: Colors.white)), // 弹幕开关 _buildDanmakuToggleButton(context), // 追番 CollectButton( bangumiItem: videoPageController.bangumiItem, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, ), MenuAnchor( consumeOutsideTap: true, onOpen: () { widget.cancelHideTimer(); playerController.canHidePlayerPanel = false; }, onClose: () { widget.cancelHideTimer(); widget.startHideTimer(); playerController.canHidePlayerPanel = true; }, builder: (BuildContext context, MenuController controller, Widget? child) { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: const Icon( Icons.more_vert, color: Colors.white, ), ); }, menuChildren: [ SubmenuButton( menuChildren: List.generate( 3, (int index) => MenuItemButton( onPressed: () => playerController.aspectRatioType = index + 1, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( index + 1 == 1 ? '自动' : index + 1 == 2 ? '裁切填充' : '拉伸填充', style: TextStyle( color: index + 1 == playerController.aspectRatioType ? Theme.of(context).colorScheme.primary : null), ), ), ), ), ), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("视频比例"), ), ), ), SubmenuButton( menuChildren: [ for (final double i in defaultPlaySpeedList) ...[ MenuItemButton( onPressed: () async { await widget.setPlaybackSpeed(i); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( '${i}x', style: TextStyle( color: i == playerController.playerSpeed ? Theme.of(context).colorScheme.primary : null), ), ), ), ), ], ], child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("倍速"), ), ), ), SubmenuButton( menuChildren: List.generate( 3, (int index) => MenuItemButton( onPressed: () => widget.handleSuperResolutionChange(index + 1), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( index + 1 == 1 ? '关闭' : index + 1 == 2 ? '效率档' : '质量档', style: TextStyle( color: playerController.superResolutionType == index + 1 ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("超分辨率"), ), ), ), SubmenuButton( menuChildren: [ MenuItemButton( child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "当前房间: ${playerController.syncplayRoom == '' ? '未加入' : playerController.syncplayRoom}"), ), ), ), MenuItemButton( child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "网络延时: ${playerController.syncplayClientRtt}ms"), ), ), ), MenuItemButton( onPressed: () { widget.showSyncPlayRoomCreateDialog(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("加入房间"), ), ), ), MenuItemButton( onPressed: () { widget.showSyncPlayEndPointSwitchDialog(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("切换服务器"), ), ), ), MenuItemButton( onPressed: () async { await playerController.exitSyncPlayRoom(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("断开连接"), ), ), ), ], child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("一起看"), ), ), ), MenuItemButton( onPressed: () { widget.showDanmakuSwitch(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("弹幕切换"), ), ), ), MenuItemButton( onPressed: () { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return DanmakuSettingsSheet( danmakuController: playerController.danmakuController, onUpdateDanmakuSpeed: playerController.updateDanmakuSpeed, ); }, ); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("弹幕设置"), ), ), ), MenuItemButton( onPressed: () { widget.showVideoInfo(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("视频详情"), ), ), ), MenuItemButton( onPressed: () { bool needRestart = playerController.playing; playerController.pause(); RemotePlay() .castVideo(playerController.videoUrl, videoPageController.currentPlugin.referer) .whenComplete(() { if (needRestart) { playerController.play(); } }); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("远程投屏"), ), ), ), MenuItemButton( onPressed: () { playerController.lanunchExternalPlayer(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("外部播放"), ), ), ), // 定时关闭 SubmenuButton( menuChildren: [ MenuItemButton( onPressed: () { TimedShutdownService().cancel(); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "不开启", style: TextStyle( color: !TimedShutdownService().isActive ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), for (final int minutes in [15, 30, 60]) MenuItemButton( onPressed: () { TimedShutdownService().start(minutes, onExpired: widget.pauseForTimedShutdown); KazumiDialog.showToast(message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭'); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( "$minutes 分钟", style: TextStyle( color: TimedShutdownService().setMinutes == minutes ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), MenuItemButton( onPressed: () { TimedShutdownService.showCustomTimerDialog( onExpired: widget.pauseForTimedShutdown, ); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text("自定义"), ), ), ), ], child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: ValueListenableBuilder( valueListenable: TimedShutdownService().remainingSecondsNotifier, builder: (context, remainingSeconds, child) { return Text( remainingSeconds > 0 ? "定时关闭 (${TimedShutdownService().formatRemainingTime()})" : "定时关闭", ); }, ), ), ), ), ], ), ], ), ); }); } } ================================================ FILE: lib/pages/plugin_editor/plugin_editor_page.dart ================================================ import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:card_settings_ui/tile/settings_tile_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/plugins/anti_crawler_config.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; class PluginEditorPage extends StatefulWidget { const PluginEditorPage({ super.key, }); @override State createState() => _PluginEditorPageState(); } class _PluginEditorPageState extends State { final PluginsController pluginsController = Modular.get(); final TextEditingController apiController = TextEditingController(); final TextEditingController typeController = TextEditingController(); final TextEditingController nameController = TextEditingController(); final TextEditingController versionController = TextEditingController(); final TextEditingController userAgentController = TextEditingController(); final TextEditingController baseURLController = TextEditingController(); final TextEditingController searchURLController = TextEditingController(); final TextEditingController searchListController = TextEditingController(); final TextEditingController searchNameController = TextEditingController(); final TextEditingController searchResultController = TextEditingController(); final TextEditingController chapterRoadsController = TextEditingController(); final TextEditingController chapterResultController = TextEditingController(); final TextEditingController refererController = TextEditingController(); bool muliSources = true; bool useWebview = true; bool useNativePlayer = true; bool usePost = false; bool useLegacyParser = false; bool adBlocker = false; // AntiCrawler fields final TextEditingController captchaImageController = TextEditingController(); final TextEditingController captchaInputController = TextEditingController(); final TextEditingController captchaButtonController = TextEditingController(); bool antiCrawlerEnabled = false; int captchaType = CaptchaType.imageCaptcha; final MenuController captchaTypeMenuController = MenuController(); static const Map _captchaTypeMap = { CaptchaType.imageCaptcha: '图片验证码', CaptchaType.autoClickButton: '自动点击按钮', }; @override void initState() { super.initState(); final Plugin plugin = Modular.args.data as Plugin; apiController.text = plugin.api; typeController.text = plugin.type; nameController.text = plugin.name; versionController.text = plugin.version; userAgentController.text = plugin.userAgent; baseURLController.text = plugin.baseUrl; searchURLController.text = plugin.searchURL; searchListController.text = plugin.searchList; searchNameController.text = plugin.searchName; searchResultController.text = plugin.searchResult; chapterRoadsController.text = plugin.chapterRoads; chapterResultController.text = plugin.chapterResult; refererController.text = plugin.referer; muliSources = plugin.muliSources; useWebview = plugin.useWebview; useNativePlayer = plugin.useNativePlayer; usePost = plugin.usePost; useLegacyParser = plugin.useLegacyParser; adBlocker = plugin.adBlocker; antiCrawlerEnabled = plugin.antiCrawlerConfig.enabled; captchaType = plugin.antiCrawlerConfig.captchaType; captchaImageController.text = plugin.antiCrawlerConfig.captchaImage; captchaInputController.text = plugin.antiCrawlerConfig.captchaInput; captchaButtonController.text = plugin.antiCrawlerConfig.captchaButton; } @override Widget build(BuildContext context) { final Plugin plugin = Modular.args.data as Plugin; final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( title: Text('规则编辑器'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( child: SizedBox( width: (MediaQuery.of(context).size.width > 1000) ? 1000 : null, child: Column( children: [ TextField( controller: nameController, decoration: const InputDecoration( labelText: 'Name', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: versionController, decoration: const InputDecoration( labelText: 'Version', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: baseURLController, decoration: const InputDecoration( labelText: 'BaseURL', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: searchURLController, decoration: const InputDecoration( labelText: 'SearchURL', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: searchListController, decoration: const InputDecoration( labelText: 'SearchList', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: searchNameController, decoration: const InputDecoration( labelText: 'SearchName', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: searchResultController, decoration: const InputDecoration( labelText: 'SearchResult', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: chapterRoadsController, decoration: const InputDecoration( labelText: 'ChapterRoads', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: chapterResultController, decoration: const InputDecoration( labelText: 'ChapterResult', border: OutlineInputBorder()), ), const SizedBox(height: 20), ExpansionTile( title: const Text('高级选项'), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), children: [ SettingsSection( title: Text('行为设置', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( title: Text('简易解析', style: TextStyle(fontFamily: fontFamily)), description: Text('使用简易解析器而不是现代解析器', style: TextStyle(fontFamily: fontFamily)), initialValue: useLegacyParser, onToggle: (v) => setState(() => useLegacyParser = v ?? !useLegacyParser), ), SettingsTile.switchTile( title: Text('POST', style: TextStyle(fontFamily: fontFamily)), description: Text('使用 POST 而不是 GET 进行检索', style: TextStyle(fontFamily: fontFamily)), initialValue: usePost, onToggle: (v) => setState(() => usePost = v ?? !usePost), ), SettingsTile.switchTile( title: Text('内置播放器', style: TextStyle(fontFamily: fontFamily)), description: Text('使用内置播放器播放视频', style: TextStyle(fontFamily: fontFamily)), initialValue: useNativePlayer, onToggle: (v) => setState(() => useNativePlayer = v ?? !useNativePlayer), ), SettingsTile.switchTile( title: Text('广告过滤', style: TextStyle(fontFamily: fontFamily)), description: Text('启用 HLS 广告过滤', style: TextStyle(fontFamily: fontFamily)), initialValue: adBlocker, onToggle: (v) => setState(() => adBlocker = v ?? !adBlocker), ), ], ), SettingsSection( title: Text('网络设置', style: TextStyle(fontFamily: fontFamily)), tiles: [ CustomSettingsTile( child: (info) => _buildTextFieldTile( context, info, controller: userAgentController, label: 'UserAgent', ), ), CustomSettingsTile( child: (info) => _buildTextFieldTile( context, info, controller: refererController, label: 'Referer', ), ), ], ), SettingsSection( title: Text('反反爬虫配置', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( title: Text('启用反反爬虫', style: TextStyle(fontFamily: fontFamily)), description: Text('检索失败时显示验证码验证按钮而非重试', style: TextStyle(fontFamily: fontFamily)), initialValue: antiCrawlerEnabled, onToggle: (v) => setState(() => antiCrawlerEnabled = v ?? !antiCrawlerEnabled), ), if (antiCrawlerEnabled) ...[ SettingsTile.navigation( onPressed: (_) { if (captchaTypeMenuController.isOpen) { captchaTypeMenuController.close(); } else { captchaTypeMenuController.open(); } }, title: Text('验证类型', style: TextStyle(fontFamily: fontFamily)), description: Text( captchaType == CaptchaType.imageCaptcha ? '图片验证码(展示验证码图片,用户手动输入)' : '自动点击验证按钮(检测到按钮后自动模拟点击)', style: TextStyle(fontFamily: fontFamily), ), value: MenuAnchor( consumeOutsideTap: true, controller: captchaTypeMenuController, builder: (_, __, ___) => Text( _captchaTypeMap[captchaType] ?? '未知', style: TextStyle(fontFamily: fontFamily), ), menuChildren: [ for (final entry in _captchaTypeMap.entries) MenuItemButton( requestFocusOnHover: false, onPressed: () => setState(() => captchaType = entry.key), child: Container( height: 48, constraints: const BoxConstraints(minWidth: 160), child: Align( alignment: Alignment.centerLeft, child: Text( entry.value, style: TextStyle( color: entry.key == captchaType ? Theme.of(context).colorScheme.primary : null, fontFamily: fontFamily, ), ), ), ), ), ], ), ), if (captchaType == CaptchaType.imageCaptcha) ...[ CustomSettingsTile( child: (info) => _buildTextFieldTile( context, info, controller: captchaImageController, label: 'CaptchaImage (XPath)', hint: '//img[@class="captcha"]', helper: '验证码图片元素的 XPath', ), ), CustomSettingsTile( child: (info) => _buildTextFieldTile( context, info, controller: captchaInputController, label: 'CaptchaInput (XPath)', hint: '//input[@name="captcha"]', helper: '验证码输入框元素的 XPath', ), ), ], CustomSettingsTile( child: (info) => _buildTextFieldTile( context, info, controller: captchaButtonController, label: captchaType == CaptchaType.imageCaptcha ? 'CaptchaButton (XPath)' : 'VerifyButton (XPath)', hint: '//button[@type="submit"]', helper: captchaType == CaptchaType.imageCaptcha ? '验证提交按钮元素的 XPath' : '验证按钮元素的 XPath,检测到后自动点击', ), ), ], ], ), ], ), ], ), ), ), ), floatingActionButton: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( heroTag: null, child: const Icon(Icons.bug_report), onPressed: () async { Plugin pluginText = Plugin( api: apiController.text, type: typeController.text, name: nameController.text, version: versionController.text, muliSources: muliSources, useWebview: useWebview, useNativePlayer: useNativePlayer, usePost: usePost, useLegacyParser: useLegacyParser, adBlocker: adBlocker, userAgent: userAgentController.text, baseUrl: baseURLController.text, searchURL: searchURLController.text, searchList: searchListController.text, searchName: searchNameController.text, searchResult: searchResultController.text, chapterRoads: chapterRoadsController.text, chapterResult: chapterResultController.text, referer: refererController.text, antiCrawlerConfig: AntiCrawlerConfig( enabled: antiCrawlerEnabled, captchaType: captchaType, captchaImage: captchaImageController.text, captchaInput: captchaInputController.text, captchaButton: captchaButtonController.text, )); Modular.to.pushNamed('/settings/plugin/test', arguments: pluginText); }, ), SizedBox(width: 15), FloatingActionButton( heroTag: null, child: const Icon(Icons.save), onPressed: () async { plugin.api = apiController.text; plugin.type = typeController.text; plugin.name = nameController.text; plugin.version = versionController.text; plugin.userAgent = userAgentController.text; plugin.baseUrl = baseURLController.text; plugin.searchURL = searchURLController.text; plugin.searchList = searchListController.text; plugin.searchName = searchNameController.text; plugin.searchResult = searchResultController.text; plugin.chapterRoads = chapterRoadsController.text; plugin.chapterResult = chapterResultController.text; plugin.muliSources = muliSources; plugin.useWebview = useWebview; plugin.useNativePlayer = useNativePlayer; plugin.usePost = usePost; plugin.useLegacyParser = useLegacyParser; plugin.adBlocker = adBlocker; plugin.referer = refererController.text; plugin.antiCrawlerConfig = AntiCrawlerConfig( enabled: antiCrawlerEnabled, captchaType: captchaType, captchaImage: captchaImageController.text, captchaInput: captchaInputController.text, captchaButton: captchaButtonController.text, ); pluginsController.updatePlugin(plugin); Navigator.of(context).pop(); }, ), ], ), ); } Widget _buildTextFieldTile( BuildContext context, SettingsTileInfo info, { required TextEditingController controller, required String label, String? hint, String? helper, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: BorderRadius.vertical( top: Radius.circular(info.isTopTile ? 20 : 3), bottom: Radius.circular(info.isBottomTile ? 20 : 3), ), child: Material( color: Theme.of(context).brightness == Brightness.light ? Theme.of(context).colorScheme.surfaceContainerLowest : Theme.of(context).colorScheme.surfaceContainerHigh, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: TextField( controller: controller, decoration: InputDecoration( labelText: label, hintText: hint, helperText: helper, border: const OutlineInputBorder(), ), ), ), ), ), if (info.needDivider) const SizedBox(height: 2), ], ); } } ================================================ FILE: lib/pages/plugin_editor/plugin_module.dart ================================================ import 'package:kazumi/pages/plugin_editor/plugin_test_page.dart'; import 'package:kazumi/pages/plugin_editor/plugin_view_page.dart'; import 'package:kazumi/pages/plugin_editor/plugin_editor_page.dart'; import 'package:kazumi/pages/plugin_editor/plugin_shop_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class PluginModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/", child: (_) => const PluginViewPage()); r.child("/shop", child: (_) => const PluginShopPage()); r.child("/test", child: (_) => const PluginTestPage(), transition: TransitionType.defaultTransition); r.child("/editor", child: (_) => const PluginEditorPage(), transition: TransitionType.defaultTransition); } } ================================================ FILE: lib/pages/plugin_editor/plugin_shop_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; class PluginShopPage extends StatefulWidget { const PluginShopPage({super.key}); @override State createState() => _PluginShopPageState(); } class _PluginShopPageState extends State { Box setting = GStorage.setting; bool timeout = false; bool loading = false; late bool enableGitProxy; // 排序方式状态:false=按更新时间排序,true=按名称排序 bool sortByName = false; final PluginsController pluginsController = Modular.get(); void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } @override void initState() { super.initState(); enableGitProxy = setting.get(SettingBoxKey.enableGitProxy, defaultValue: false); } // 刷新规则列表 void _handleRefresh() async { if (!loading) { setState(() { loading = true; timeout = false; }); enableGitProxy = setting.get(SettingBoxKey.enableGitProxy, defaultValue: false); pluginsController.queryPluginHTTPList().then((_) { setState(() { loading = false; }); if (pluginsController.pluginHTTPList.isEmpty) { setState(() { timeout = true; }); } }); } } // 切换排序方式 void _toggleSort() { setState(() { sortByName = !sortByName; }); } Widget get pluginHTTPListBody { return Observer(builder: (context) { // 创建列表副本用于排序 var sortedList = List.from(pluginsController.pluginHTTPList); // 排序规则: // 1. 按名称排序:忽略大小写的字母顺序 // 2. 按时间排序:更新时间降序(最新的在前面) if (sortByName) { sortedList.sort( (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } else { sortedList.sort((a, b) => b.lastUpdate.compareTo(a.lastUpdate)); } return ListView.builder( itemCount: sortedList.length, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.fromLTRB(8, 0, 8, 8), child: ListTile( title: Row( children: [ Text( sortedList[index].name, style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 1.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(16.0), ), child: Text( sortedList[index].version, style: TextStyle( color: Theme.of(context).colorScheme.surface), ), ), const SizedBox(width: 5), Container( padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 1.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(16.0), ), child: Text( sortedList[index].useNativePlayer ? "native" : "webview", style: TextStyle( color: Theme.of(context).colorScheme.surface), ), ), if (sortedList[index].antiCrawlerEnabled) ...[ const SizedBox(width: 5), Container( padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 1.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.tertiary, borderRadius: BorderRadius.circular(16.0), ), child: Text( 'captcha', style: TextStyle( color: Theme.of(context) .colorScheme .onTertiary), ), ), ], ], ), if (sortedList[index].lastUpdate > 0) ...[ const SizedBox(height: 4), Text( '更新时间: ${DateTime.fromMillisecondsSinceEpoch(sortedList[index].lastUpdate).toString().split('.')[0]}', style: const TextStyle(color: Colors.grey), ), ], ], ), trailing: TextButton( onPressed: () async { if (pluginsController.pluginStatus(sortedList[index]) == 'install') { KazumiDialog.showToast(message: '导入中'); int res = await pluginsController .tryUpdatePluginByName(sortedList[index].name); if (res == 0) { KazumiDialog.showToast(message: '导入成功'); setState(() {}); } else if (res == 1) { KazumiDialog.showToast( message: 'kazumi版本过低, 此规则不兼容当前版本'); } else if (res == 2) { KazumiDialog.showToast(message: '导入规则失败'); } } if (pluginsController.pluginStatus(sortedList[index]) == 'update') { KazumiDialog.showToast(message: '更新中'); int res = await pluginsController .tryUpdatePluginByName(sortedList[index].name); if (res == 0) { KazumiDialog.showToast(message: '更新成功'); setState(() {}); } else if (res == 1) { KazumiDialog.showToast( message: 'kazumi版本过低, 此规则不兼容当前版本'); } else if (res == 2) { KazumiDialog.showToast(message: '更新规则失败'); } } }, child: Text(pluginsController .pluginStatus(sortedList[index]) == 'install' ? '安装' : (pluginsController.pluginStatus(sortedList[index]) == 'installed') ? '已安装' : '更新'), )), ); }, ); }); } Widget get timeoutWidget { return Center( child: GeneralErrorWidget( errMsg: '啊咧(⊙.⊙) 无法访问远程仓库\n${enableGitProxy ? '镜像已启用' : '镜像已禁用'}', actions: [ GeneralErrorButton( onPressed: () { Modular.to.pushNamed('/settings/webdav/'); }, text: enableGitProxy ? '禁用镜像' : '启用镜像', ), GeneralErrorButton( onPressed: () { _handleRefresh(); }, text: '刷新', ), ], ), ); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) {}); return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: Scaffold( appBar: SysAppBar( title: const Text('规则仓库'), actions: [ IconButton( onPressed: _toggleSort, tooltip: sortByName ? '按名称排序' : '按更新时间排序', icon: Icon(sortByName ? Icons.sort_by_alpha : Icons.access_time)), IconButton( onPressed: () { _handleRefresh(); }, tooltip: '刷新规则列表', icon: const Icon(Icons.refresh)) ], ), body: loading ? (const Center(child: CircularProgressIndicator())) : (pluginsController.pluginHTTPList.isEmpty ? timeoutWidget : pluginHTTPListBody), ), ); } } ================================================ FILE: lib/pages/plugin_editor/plugin_test_page.dart ================================================ import 'package:dio/dio.dart'; import 'package:flutter/material.dart' hide Element; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/modules/search/plugin_search_module.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:html/dom.dart' show Element; import 'package:html/parser.dart' show parse; import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart'; import '../../modules/roads/road_module.dart'; import '../../plugins/plugins.dart'; const _h8 = SizedBox(height: 8.0); const _h12 = SizedBox(height: 12.0); // 简化配色映射:仅三类核心色 enum CoreColorType { error, success, waiting } extension CoreColorExtension on ThemeData { Color getCoreColor(CoreColorType type) { switch (type) { case CoreColorType.error: return colorScheme.error; case CoreColorType.success: return colorScheme.primary; case CoreColorType.waiting: return colorScheme.onSurfaceVariant; } } } class PluginTestPage extends StatefulWidget { const PluginTestPage({super.key}); @override State createState() => _PluginTestPageState(); } class _PluginTestPageState extends State { late final Plugin plugin; final VideoPageController videoPageController = Modular.get(); final testKeywordController = TextEditingController(); final htmlScrollController = ScrollController(); final chapterScrollController = ScrollController(); final itemHtmlScrollController = ScrollController(); String searchHtml = ""; PluginSearchResponse? searchRes; List? chapters; bool isTesting = false; String errorMsg = ""; final Map _itemHtmlMap = {}; int? _showItemHtmlIdx; bool get _hasSearchHtml => searchHtml.isNotEmpty; bool get _hasSearchData => searchRes?.data.isNotEmpty ?? false; bool get _hasChapters => chapters?.isNotEmpty ?? false; bool get _needChapterParse => plugin.chapterRoads.isNotEmpty; CancelToken? _testSearchRequestCancelToken; CancelToken? _testRoadsCancelToken; @override void initState() { super.initState(); plugin = Modular.args.data as Plugin; testKeywordController.addListener( () => errorMsg.isNotEmpty ? setState(() => errorMsg = "") : null); } @override void dispose() { _testSearchRequestCancelToken?.cancel(); _testRoadsCancelToken?.cancel(); testKeywordController.dispose(); htmlScrollController.dispose(); chapterScrollController.dispose(); itemHtmlScrollController.dispose(); super.dispose(); } void onBackPressed() => KazumiDialog.observer.hasKazumiDialog ? KazumiDialog.dismiss() : null; void resetState() => setState(() { _testSearchRequestCancelToken?.cancel(); _testSearchRequestCancelToken = null; _testRoadsCancelToken?.cancel(); _testRoadsCancelToken = null; searchHtml = ""; searchRes = null; chapters = null; errorMsg = ""; _itemHtmlMap.clear(); _showItemHtmlIdx = null; }); String _parseItemHtml(int index) { if (_itemHtmlMap.containsKey(index)) return _itemHtmlMap[index]!; try { final node = (parse(searchHtml) .documentElement! .queryXPath(plugin.searchList) .nodes[index] .node as Element); return _itemHtmlMap[index] = node.outerHtml; } catch (e) { KazumiLogger().e('PluginTest: failed to parse HTML item ${index + 1}', error: e); return "解析失败:$e"; } } void _toggleItemHtml(int index) { if (_showItemHtmlIdx == index) return setState(() => _showItemHtmlIdx = null); setState(() => isTesting = true); _parseItemHtml(index); setState(() { _showItemHtmlIdx = index; isTesting = false; }); } Future startTest() async { final keyword = testKeywordController.text.trim(); resetState(); setState(() => isTesting = true); try { _testSearchRequestCancelToken?.cancel(); _testSearchRequestCancelToken = CancelToken(); searchHtml = await plugin.testSearchRequest(keyword, shouldRethrow: true, cancelToken: _testSearchRequestCancelToken); searchRes = plugin.testQueryBangumi(searchHtml); if (_hasSearchData && _needChapterParse) { final firstItem = searchRes!.data.first; if (firstItem.src.isNotEmpty) { _testRoadsCancelToken?.cancel(); _testRoadsCancelToken = CancelToken(); chapters = await plugin.querychapterRoads(firstItem.src, cancelToken: _testRoadsCancelToken); } } } catch (e, stack) { KazumiLogger().e("PluginTest: test failed", error: e, stackTrace: stack); } finally { if (mounted) setState(() => isTesting = false); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return PopScope( canPop: true, onPopInvokedWithResult: (didPop, _) => !didPop ? onBackPressed() : null, child: Scaffold( appBar: SysAppBar( title: Text('${plugin.name} 测试'), actions: [ IconButton( onPressed: isTesting ? null : startTest, icon: const Icon(Icons.bug_report_outlined), tooltip: '开始测试', ), IconButton( onPressed: resetState, icon: const Icon(Icons.refresh), tooltip: '重置', ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1000), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildKeywordInput(theme), _h12, _buildErrorWidget(theme), _buildExpansionTile( theme: theme, title: '1. 搜索请求测试', subtitle: _getSearchSubtitle(), expanded: false, child: _buildSearchContent(theme), ), _h12, _buildExpansionTile( theme: theme, title: '2. 搜索解析测试', subtitle: _getParseSubtitle(), expanded: false, child: _buildParseContent(theme), ), _h12, _buildExpansionTile( theme: theme, title: '3. 章节列表测试', subtitle: _getChapterSubtitle(), expanded: _hasSearchData, child: _buildChapterContent(theme), ), ]), ), ), ), ), ); } Widget _buildExpansionTile({ required ThemeData theme, required String title, required String subtitle, required bool expanded, required Widget child, }) { return ExpansionTile( title: Text(title, style: theme.textTheme.titleMedium), subtitle: Text(subtitle, style: TextStyle( fontSize: 12.0, color: _getSubtitleColor(subtitle, theme))), initiallyExpanded: expanded, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), iconColor: theme.getCoreColor(CoreColorType.success), collapsedIconColor: theme.getCoreColor(CoreColorType.waiting), children: [_h8, child, _h8], ); } Widget _buildKeywordInput(ThemeData theme) => TextField( controller: testKeywordController, decoration: InputDecoration( labelText: '测试关键词', border: OutlineInputBorder( borderSide: BorderSide(color: theme.getCoreColor(CoreColorType.waiting))), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: theme.getCoreColor(CoreColorType.success))), labelStyle: TextStyle(color: theme.getCoreColor(CoreColorType.waiting)), ), enabled: !isTesting, onSubmitted: (_) => startTest(), style: theme.textTheme.bodyLarge, ); Widget _buildErrorWidget(ThemeData theme) => errorMsg.isEmpty || isTesting ? const SizedBox.shrink() : Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: theme.colorScheme.errorContainer, border: Border.all(color: theme.getCoreColor(CoreColorType.error)), borderRadius: BorderRadius.circular(8), ), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.error_outline, color: theme.getCoreColor(CoreColorType.error), size: 20), _h8, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(errorMsg, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onErrorContainer)), _h8, TextButton( onPressed: startTest, style: TextButton.styleFrom( backgroundColor: theme .getCoreColor(CoreColorType.error) .withOpacity(0.1)), child: Text('重试测试', style: TextStyle( color: theme.colorScheme.onErrorContainer)), ), ]), ), ]), ); Widget _buildLoading(ThemeData theme) => Center( child: CircularProgressIndicator.adaptive( valueColor: AlwaysStoppedAnimation( theme.getCoreColor(CoreColorType.success)), ), ); Widget _buildEmpty(String text, ThemeData theme, {bool isError = false}) => Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( text, style: theme.textTheme.bodyMedium?.copyWith( color: isError ? theme.getCoreColor(CoreColorType.error) : theme.getCoreColor(CoreColorType.waiting), ), ), ), ); String _getSearchSubtitle() { if (isTesting) return '测试中...'; if (!_hasSearchHtml) return '未执行测试'; return 'HTML长度:${searchHtml.length} 字符'; } // 简化副标题颜色逻辑:仅三类 Color _getSubtitleColor(String subtitle, ThemeData theme) { if (subtitle.contains('测试中') || subtitle.contains('获取中') || subtitle.contains('解析中')) { return theme.getCoreColor(CoreColorType.waiting); } if (subtitle.contains('失败') || subtitle.contains('无可用') || subtitle.contains('无有效')) { return theme.getCoreColor(CoreColorType.error); } return theme.getCoreColor(CoreColorType.success); } Widget _buildSearchContent(ThemeData theme) { if (isTesting) return _buildLoading(theme); if (!_hasSearchHtml) return _buildEmpty('点击顶部「开始测试」按钮执行', theme); return Container( margin: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: theme.getCoreColor(CoreColorType.waiting)), color: theme.colorScheme.surface, ), height: 250, child: SingleChildScrollView( controller: htmlScrollController, physics: const ClampingScrollPhysics(), child: SelectableText( searchHtml, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), ), ), ); } String _getParseSubtitle() { if (isTesting && _showItemHtmlIdx == null) return '解析中...'; if (!_hasSearchHtml) return '未执行解析'; if (!_hasSearchData) return '未解析到结果'; return '解析到 ${searchRes?.data.length ?? 0} 条结果'; } Widget _buildParseContent(ThemeData theme) { if (isTesting && _showItemHtmlIdx == null) return _buildLoading(theme); if (!_hasSearchHtml) return _buildEmpty('请先完成搜索请求测试', theme); if (!_hasSearchData) return _buildEmpty('未解析到搜索结果', theme, isError: true); return Column(children: [ ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: searchRes!.data.length, itemBuilder: (_, i) => _buildSearchItemCard(searchRes!.data[i], i, theme), ), _h8, ]); } Widget _buildSearchItemCard(SearchItem item, int i, ThemeData theme) { final isShowHtml = _showItemHtmlIdx == i; final itemHtml = _itemHtmlMap[i] ?? '加载中...'; return Column(children: [ Card( margin: const EdgeInsets.only(bottom: 8.0), elevation: 1, child: Padding( padding: const EdgeInsets.all(12.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${i + 1}:${item.name}', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), IconButton( onPressed: isTesting ? null : () => _toggleItemHtml(i), icon: Icon( isShowHtml ? Icons.keyboard_arrow_up : Icons.code, size: 18, color: theme.getCoreColor(CoreColorType.success), ), tooltip: isShowHtml ? '隐藏HTML' : '查看HTML', ), ]), _h8, Text('链接:${item.src}', style: theme.textTheme.bodySmall?.copyWith( color: theme.getCoreColor(CoreColorType.waiting))), ]), ), ), if (isShowHtml) Container( margin: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: theme.getCoreColor(CoreColorType.waiting)), color: theme.colorScheme.surface, ), height: 250, child: SingleChildScrollView( controller: itemHtmlScrollController, physics: const ClampingScrollPhysics(), child: SelectableText( itemHtml, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), ), ), ), ]); } String _getChapterSubtitle() { if (isTesting) return '获取中...'; if (!_hasSearchData) return '无有效搜索结果'; if (!_needChapterParse) return '无需解析章节'; if (chapters == null) return '未获取章节数据'; return '获取到 ${chapters?.length ?? 0} 个播放列表'; } Widget _buildChapterContent(ThemeData theme) { if (!_needChapterParse) return _buildEmpty('未填写章节规则', theme); if (isTesting) return _buildLoading(theme); if (!_hasSearchData) return _buildEmpty('请先解析到有效结果', theme); if (chapters == null) return _buildEmpty('未获取章节数据', theme, isError: true); if (!_hasChapters) return _buildEmpty('无可用章节', theme, isError: true); return Container( padding: const EdgeInsets.all(8.0), height: 280, child: ListView.builder( controller: chapterScrollController, itemCount: chapters?.length ?? 0, itemBuilder: (_, i) => _buildChapterCard(chapters![i], i, theme), ), ); } Widget _buildChapterCard(Road road, int i, ThemeData theme) => Card( margin: const EdgeInsets.only(bottom: 8.0), elevation: 1, child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '播放列表 ${i + 1}:${road.name}', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w500), ), _h8, Text('章节数量:${road.data.length}', style: theme.textTheme.bodySmall?.copyWith( color: theme.getCoreColor(CoreColorType.waiting))), _h8, SizedBox( width: double.infinity, height: 120, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...road.identifier.asMap().entries.map((e) => Text( '${e.key + 1}. ${e.value}', style: theme.textTheme.bodySmall, )), ]), ), ), ]), ), ); } ================================================ FILE: lib/pages/plugin_editor/plugin_view_page.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; class PluginViewPage extends StatefulWidget { const PluginViewPage({super.key}); @override State createState() => _PluginViewPageState(); } class _PluginViewPageState extends State { final PluginsController pluginsController = Modular.get(); // 是否处于多选模式 bool isMultiSelectMode = false; // 已选中的规则名称集合 final Set selectedNames = {}; Future _handleUpdate() async { KazumiDialog.showLoading(msg: '更新中'); int count = await pluginsController.tryUpdateAllPlugin(); KazumiDialog.dismiss(); if (count == 0) { KazumiDialog.showToast(message: '所有规则已是最新'); } else { KazumiDialog.showToast(message: '更新成功 $count 条'); } } void _handleAdd() { KazumiDialog.show(builder: (context) { return AlertDialog( // contentPadding: EdgeInsets.zero, // 设置为零以减小内边距 content: SingleChildScrollView( // 使用可滚动的SingleChildScrollView包装Column child: Column( mainAxisSize: MainAxisSize.min, // 设置为MainAxisSize.min以减小高度 children: [ ListTile( title: const Text('新建规则'), onTap: () { KazumiDialog.dismiss(); Modular.to.pushNamed('/settings/plugin/editor', arguments: Plugin.fromTemplate()); }, ), const SizedBox(height: 10), ListTile( title: const Text('从规则仓库导入'), onTap: () { KazumiDialog.dismiss(); Modular.to.pushNamed('/settings/plugin/shop', arguments: Plugin.fromTemplate()); }, ), const SizedBox(height: 10), ListTile( title: const Text('从剪贴板导入'), onTap: () { KazumiDialog.dismiss(); _showInputDialog(); }, ), ], ), ), ); }); } void _showInputDialog() { final TextEditingController textController = TextEditingController(); KazumiDialog.show(builder: (context) { return AlertDialog( title: const Text('导入规则'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( controller: textController, ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextButton( onPressed: () async { final String msg = textController.text; try { pluginsController.updatePlugin(Plugin.fromJson( json.decode(Utils.kazumiBase64ToJson(msg)))); KazumiDialog.showToast(message: '导入成功'); } catch (e) { KazumiDialog.dismiss(); KazumiDialog.showToast(message: '导入失败 ${e.toString()}'); } KazumiDialog.dismiss(); }, child: const Text('导入'), ); }) ], ); }); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) {}); return PopScope( canPop: !isMultiSelectMode, onPopInvokedWithResult: (bool didPop, Object? result) { if (isMultiSelectMode) { setState(() { isMultiSelectMode = false; selectedNames.clear(); }); return; } onBackPressed(context); }, child: Scaffold( appBar: SysAppBar( title: isMultiSelectMode ? Text('已选择 ${selectedNames.length} 项') : const Text('规则管理'), leading: isMultiSelectMode ? IconButton( icon: const Icon(Icons.close), onPressed: () { setState(() { isMultiSelectMode = false; selectedNames.clear(); }); }, ) : null, actions: [ if (isMultiSelectMode) ...[ IconButton( onPressed: selectedNames.isEmpty ? null : () { KazumiDialog.show( builder: (context) => AlertDialog( title: const Text('删除规则'), content: Text('确定要删除选中的 ${selectedNames.length} 条规则吗?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle( color: Theme.of(context) .colorScheme .outline), ), ), TextButton( onPressed: () { pluginsController .removePlugins(selectedNames); setState(() { isMultiSelectMode = false; selectedNames.clear(); }); KazumiDialog.dismiss(); }, child: const Text('删除'), ), ], ), ); }, icon: const Icon(Icons.delete), ), ] else ...[ IconButton( onPressed: () { _handleUpdate(); }, tooltip: '更新全部', icon: const Icon(Icons.update), ), IconButton( onPressed: () { _handleAdd(); }, tooltip: '添加规则', icon: const Icon(Icons.add), ) ], ], ), body: Observer(builder: (context) { return pluginsController.pluginList.isEmpty ? const Center( child: Text('啊咧(⊙.⊙) 没有可用规则的说'), ) : Builder(builder: (context) { return ReorderableListView.builder( buildDefaultDragHandles: false, proxyDecorator: (child, index, animation) { return Material( elevation: 0, color: Colors.transparent, child: child, ); }, onReorder: (int oldIndex, int newIndex) { pluginsController.onReorder(oldIndex, newIndex); }, itemCount: pluginsController.pluginList.length, itemBuilder: (context, index) { var plugin = pluginsController.pluginList[index]; bool canUpdate = pluginsController.pluginUpdateStatus(plugin) == 'updatable'; return Card( key: ValueKey(index), margin: const EdgeInsets.fromLTRB(8, 0, 8, 8), child: ListTile( trailing: pluginCardTrailing(index), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), onLongPress: () { if (!isMultiSelectMode) { setState(() { isMultiSelectMode = true; selectedNames.add(plugin.name); }); } }, onTap: () { if (isMultiSelectMode) { setState(() { if (selectedNames.contains(plugin.name)) { selectedNames.remove(plugin.name); if (selectedNames.isEmpty) { isMultiSelectMode = false; } } else { selectedNames.add(plugin.name); } }); } }, selected: selectedNames.contains(plugin.name), selectedTileColor: Theme.of(context) .colorScheme .primaryContainer, title: Text( plugin.name, style: const TextStyle( fontWeight: FontWeight.bold), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Version: ${plugin.version}', style: const TextStyle(color: Colors.grey), ), if (canUpdate) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .errorContainer, borderRadius: BorderRadius.circular(4), ), child: Text( '可更新', style: TextStyle( fontSize: 12, color: Theme.of(context) .colorScheme .onErrorContainer, ), ), ), ], if (pluginsController.validityTracker .isSearchValid(plugin.name)) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .tertiaryContainer, borderRadius: BorderRadius.circular(4), ), child: Text( '搜索有效', style: TextStyle( fontSize: 12, color: Theme.of(context) .colorScheme .onTertiaryContainer, ), ), ), ], ], ), ], ), )); }); }); }), ), ); } Widget pluginCardTrailing(int index) { final plugin = pluginsController.pluginList[index]; return Row(mainAxisSize: MainAxisSize.min, children: [ isMultiSelectMode ? Checkbox( value: selectedNames.contains(plugin.name), onChanged: (bool? value) { setState(() { if (value == true) { selectedNames.add(plugin.name); } else { selectedNames.remove(plugin.name); if (selectedNames.isEmpty) { isMultiSelectMode = false; } } }); }, ) : popupMenuButton(index), ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle), // 单独的拖拽按钮 ) ]); } Widget popupMenuButton(int index) { final plugin = pluginsController.pluginList[index]; return MenuAnchor( consumeOutsideTap: true, builder: (BuildContext context, MenuController controller, Widget? child) { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: const Icon(Icons.more_vert), ); }, menuChildren: [ MenuItemButton( requestFocusOnHover: false, onPressed: () async { var state = pluginsController.pluginUpdateStatus(plugin); if (state == "nonexistent") { KazumiDialog.showToast(message: '规则仓库中没有当前规则'); } else if (state == "latest") { KazumiDialog.showToast(message: '规则已是最新'); } else if (state == "updatable") { KazumiDialog.showLoading(msg: '更新中'); int res = await pluginsController.tryUpdatePlugin(plugin); KazumiDialog.dismiss(); if (res == 0) { KazumiDialog.showToast(message: '更新成功'); } else if (res == 1) { KazumiDialog.showToast(message: 'kazumi版本过低, 此规则不兼容当前版本'); } else if (res == 2) { KazumiDialog.showToast(message: '更新规则失败'); } } }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Icons.update_rounded), SizedBox(width: 8), Text('更新'), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () { Modular.to.pushNamed('/settings/plugin/editor', arguments: plugin); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Icons.edit), SizedBox(width: 8), Text('编辑'), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () { Modular.to.pushNamed('/settings/plugin/test', arguments: plugin); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Icons.bug_report_outlined), SizedBox(width: 8), Text('测试'), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () { KazumiDialog.show(builder: (context) { return AlertDialog( title: const Text('规则链接'), content: SelectableText( Utils.jsonToKazumiBase64(json .encode(pluginsController.pluginList[index].toJson())), style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { Clipboard.setData(ClipboardData( text: Utils.jsonToKazumiBase64( json.encode( pluginsController.pluginList[index].toJson(), ), ), )); KazumiDialog.dismiss(); }, child: const Text('复制到剪贴板'), ), ], ); }); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Icons.share), SizedBox(width: 8), Text('分享'), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () async { setState(() { pluginsController.removePlugin(plugin); }); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon(Icons.delete), SizedBox(width: 8), Text('删除'), ], ), ), ), ), ], ); } } ================================================ FILE: lib/pages/popular/popular_controller.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:mobx/mobx.dart'; part 'popular_controller.g.dart'; class PopularController = _PopularController with _$PopularController; abstract class _PopularController with Store { final ScrollController scrollController = ScrollController(); @observable String currentTag = ''; @observable ObservableList bangumiList = ObservableList.of([]); @observable ObservableList trendList = ObservableList.of([]); double scrollOffset = 0.0; @observable bool isLoadingMore = false; @observable bool isTimeOut = false; void setCurrentTag(String s) { currentTag = s; } void clearBangumiList() { bangumiList.clear(); } Future queryBangumiByTrend({String type = 'add'}) async { if (type == 'init') { trendList.clear(); } isLoadingMore = true; var result = await BangumiHTTP.getBangumiTrendsList(offset: trendList.length); trendList.addAll(result); isLoadingMore = false; isTimeOut = trendList.isEmpty; } Future queryBangumiByTag({String type = 'add'}) async { if (type == 'init') { bangumiList.clear(); } isLoadingMore = true; int randomNumber = Random().nextInt(8000) + 1; var tag = currentTag; var result = await BangumiHTTP.getBangumiList(rank: randomNumber, tag: tag); bangumiList.addAll(result); isLoadingMore = false; isTimeOut = bangumiList.isEmpty; } } ================================================ FILE: lib/pages/popular/popular_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'popular_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$PopularController on _PopularController, Store { late final _$currentTagAtom = Atom(name: '_PopularController.currentTag', context: context); @override String get currentTag { _$currentTagAtom.reportRead(); return super.currentTag; } @override set currentTag(String value) { _$currentTagAtom.reportWrite(value, super.currentTag, () { super.currentTag = value; }); } late final _$bangumiListAtom = Atom(name: '_PopularController.bangumiList', context: context); @override ObservableList get bangumiList { _$bangumiListAtom.reportRead(); return super.bangumiList; } @override set bangumiList(ObservableList value) { _$bangumiListAtom.reportWrite(value, super.bangumiList, () { super.bangumiList = value; }); } late final _$trendListAtom = Atom(name: '_PopularController.trendList', context: context); @override ObservableList get trendList { _$trendListAtom.reportRead(); return super.trendList; } @override set trendList(ObservableList value) { _$trendListAtom.reportWrite(value, super.trendList, () { super.trendList = value; }); } late final _$isLoadingMoreAtom = Atom(name: '_PopularController.isLoadingMore', context: context); @override bool get isLoadingMore { _$isLoadingMoreAtom.reportRead(); return super.isLoadingMore; } @override set isLoadingMore(bool value) { _$isLoadingMoreAtom.reportWrite(value, super.isLoadingMore, () { super.isLoadingMore = value; }); } late final _$isTimeOutAtom = Atom(name: '_PopularController.isTimeOut', context: context); @override bool get isTimeOut { _$isTimeOutAtom.reportRead(); return super.isTimeOut; } @override set isTimeOut(bool value) { _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () { super.isTimeOut = value; }); } @override String toString() { return ''' currentTag: ${currentTag}, bangumiList: ${bangumiList}, trendList: ${trendList}, isLoadingMore: ${isLoadingMore}, isTimeOut: ${isTimeOut} '''; } } ================================================ FILE: lib/pages/popular/popular_module.dart ================================================ import 'package:kazumi/pages/popular/popular_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class PopularModule extends Module { @override void routes(r) { r.child("/", child: (_) => const PopularPage()); } } ================================================ FILE: lib/pages/popular/popular_page.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'package:kazumi/bean/widget/custom_dropdown_menu.dart'; import 'package:kazumi/pages/popular/popular_controller.dart'; import 'package:kazumi/bean/card/bangumi_card.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/services.dart'; import 'package:window_manager/window_manager.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/pages/menu/menu.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb; class PopularPage extends StatefulWidget { const PopularPage({super.key}); @override State createState() => _PopularPageState(); } class _PopularPageState extends State with AutomaticKeepAliveClientMixin { DateTime? _lastPressedAt; late NavigationBarState navigationBarState; final FocusNode _focusNode = FocusNode(); final ScrollController scrollController = ScrollController(); final PopularController popularController = Modular.get(); // Key used to position the dropdown menu for the tag selector final GlobalKey selectorKey = GlobalKey(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); scrollController.addListener(scrollListener); if (popularController.trendList.isEmpty) { popularController.queryBangumiByTrend(); } } @override void didChangeDependencies() { super.didChangeDependencies(); } @override void dispose() { _focusNode.dispose(); scrollController.removeListener(scrollListener); super.dispose(); } void scrollListener() { popularController.scrollOffset = scrollController.offset; if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200 && !popularController.isLoadingMore) { KazumiLogger().i('PopularPageController: Fetching next recommendation batch'); if (popularController.currentTag != '') { popularController.queryBangumiByTag(); } else { popularController.queryBangumiByTrend(); } } } bool showWindowButton() { return GStorage.setting .get(SettingBoxKey.showWindowButton, defaultValue: false); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } if (_lastPressedAt == null || DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 2)) { _lastPressedAt = DateTime.now(); KazumiDialog.showToast(message: "再按一次退出应用", context: context); return; } SystemNavigator.pop(); } @override Widget build(BuildContext context) { super.build(context); return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: Scaffold( body: CustomScrollView( controller: scrollController, slivers: [ buildSliverAppBar(), SliverToBoxAdapter( child: Observer( builder: (_) => AnimatedOpacity( opacity: popularController.isLoadingMore ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: popularController.isLoadingMore ? const LinearProgressIndicator(minHeight: 4) : const SizedBox(height: 4), ), ), ), SliverPadding( padding: const EdgeInsets.fromLTRB( StyleString.cardSpace, 0, StyleString.cardSpace, 0), sliver: Observer(builder: (_) { if (popularController.isTimeOut) { return SliverToBoxAdapter( child: SizedBox( height: 400, child: GeneralErrorWidget( errMsg: '什么都没有找到 (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { if (popularController.trendList.isEmpty) { popularController.queryBangumiByTrend(); } else { popularController.queryBangumiByTag(); } }, text: '点击重试', ), ], ), ), ); } return contentGrid( (popularController.currentTag == '') ? popularController.trendList : popularController.bangumiList, ); })), ], ), floatingActionButton: FloatingActionButton( onPressed: () => scrollController.animateTo(0, duration: const Duration(milliseconds: 350), curve: Curves.easeOut), child: const Icon(Icons.arrow_upward), ), ), ); } Widget contentGrid(bangumiList) { int crossCount = 3; if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) { crossCount = 5; } if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) { crossCount = 6; } return SliverPadding( padding: const EdgeInsets.all(8), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // 行间距 mainAxisSpacing: StyleString.cardSpace - 2, // 列间距 crossAxisSpacing: StyleString.cardSpace, // 列数 crossAxisCount: crossCount, mainAxisExtent: MediaQuery.of(context).size.width / crossCount / 0.65 + MediaQuery.textScalerOf(context).scale(32.0), ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return bangumiList!.isNotEmpty ? BangumiCardV(bangumiItem: bangumiList[index]) : null; }, childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10, ), ), ); } Widget buildSliverAppBar() { final theme = Theme.of(context); return SliverAppBar( pinned: true, stretch: true, expandedHeight: 120, elevation: 0, titleSpacing: 0, centerTitle: false, backgroundColor: Theme.of(context).colorScheme.surface, actions: buildActions(), title: null, flexibleSpace: SafeArea( child: dtb.DragToMoveArea( child: LayoutBuilder( builder: (context, constraints) { final double maxExtent = 120 - MediaQuery.of(context).padding.top; final t = (1 - ((constraints.maxHeight - kToolbarHeight) / (maxExtent - kToolbarHeight)) .clamp(0.0, 1.0)); // 字重收缩后为 w500,展开时为 w700 final fontWeight = t < 0.5 ? FontWeight.w700 : FontWeight.w500; final fontSize = lerpDouble(28, 20, t)!; return Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only( left: 16, top: 8, bottom: 8, right: 60), child: SizedBox( height: 44, child: Observer( builder: (_) { final bool isTrend = popularController.currentTag == ''; return InkWell( key: selectorKey, borderRadius: BorderRadius.circular(8), onTap: showTagMenu, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( isTrend ? '热门番组' : popularController.currentTag, style: theme.textTheme.headlineMedium!.copyWith( fontWeight: fontWeight, fontSize: fontSize, ), ), const SizedBox(width: 4), Icon(Icons.keyboard_arrow_down, size: fontSize, color: theme.iconTheme.color), ], ), ); }, ), ), ), ); }, ), ), ), ); } List buildActions() { final actions = [ if (MediaQuery.of(context).orientation == Orientation.portrait) IconButton( tooltip: '搜索', onPressed: () => Modular.to.pushNamed('/search/'), icon: const Icon(Icons.search), ), ]; actions.add( IconButton( tooltip: '历史记录', onPressed: () => Modular.to.pushNamed('/settings/history/'), icon: const Icon(Icons.history), ), ); if (Utils.isDesktop()) { if (!showWindowButton()) { actions.add( IconButton( tooltip: '退出', onPressed: () => windowManager.close(), icon: const Icon(Icons.close), ), ); } } return actions; } Future showTagMenu() async { // Calculate the position of the button manually to position the dropdown menu. // Using CustomDropdownMenu instead of PopupMenuButton to avoid flickering issues // and to support different font sizes in the button and menu items. final RenderBox renderBox = selectorKey.currentContext!.findRenderObject() as RenderBox; final Offset offset = renderBox.localToGlobal(Offset.zero); final Size size = renderBox.size; final selected = await Navigator.push( context, PageRouteBuilder( opaque: false, barrierDismissible: true, barrierColor: Colors.transparent, pageBuilder: (context, animation, secondaryAnimation) { return CustomDropdownMenu( offset: offset, buttonSize: size, animation: animation, maxWidth: 80, items: [ '', ...defaultAnimeTags, ], itemBuilder: (item) => item.isEmpty ? '热门番组' : item, ); }, transitionDuration: const Duration(milliseconds: 200), reverseTransitionDuration: const Duration(milliseconds: 150), ), ); if (selected == null) return; if (selected == '' && popularController.currentTag != '') { scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeOut); popularController.setCurrentTag(''); popularController.clearBangumiList(); if (popularController.trendList.isEmpty) { await popularController.queryBangumiByTrend(); } } else if (selected != '' && selected != popularController.currentTag) { scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeOut); popularController.setCurrentTag(selected); await popularController.queryBangumiByTag(type: 'init'); } } } ================================================ FILE: lib/pages/router.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/popular/popular_module.dart'; import 'package:kazumi/pages/my/my_module.dart'; import 'package:kazumi/pages/timeline/timeline_module.dart'; import 'package:kazumi/pages/collect/collect_module.dart'; class MenuRouteItem { final String path; final Module module; const MenuRouteItem({ required this.path, required this.module, }); } class MenuRoute { final List menuList; const MenuRoute(this.menuList); int get size => menuList.length; List get moduleList { return menuList.map((e) => e.module).toList(); } List get routes { return menuList.map((e) => ModuleRoute(e.path, module: e.module)).toList(); } getPath(int index) { return menuList[index].path; } } final MenuRoute menu = MenuRoute([ MenuRouteItem( path: "/popular", module: PopularModule(), ), MenuRouteItem( path: "/timeline", module: TimelineModule(), ), MenuRouteItem( path: "/collect", module: CollectModule(), ), MenuRouteItem( path: "/my", module: MyModule(), ), ]); ================================================ FILE: lib/pages/search/search_controller.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:mobx/mobx.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:kazumi/utils/search_parser.dart'; import 'package:kazumi/modules/search/search_history_module.dart'; import 'package:kazumi/repositories/collect_repository.dart'; import 'package:kazumi/repositories/search_history_repository.dart'; import 'package:kazumi/modules/collect/collect_type.dart'; part 'search_controller.g.dart'; class SearchPageController = _SearchPageController with _$SearchPageController; abstract class _SearchPageController with Store { final _collectRepository = Modular.get(); final _searchHistoryRepository = Modular.get(); @observable bool isLoading = false; @observable bool isTimeOut = false; @observable late bool notShowWatchedBangumis = _collectRepository.getSearchNotShowWatchedBangumis(); @observable late bool notShowAbandonedBangumis = _collectRepository.getSearchNotShowAbandonedBangumis(); @observable ObservableList bangumiList = ObservableList.of([]); @observable ObservableList searchHistories = ObservableList.of([]); @action void loadSearchHistories() { final histories = _searchHistoryRepository.getAllHistories(); searchHistories.clear(); searchHistories.addAll(histories); } /// Avaliable sort parameters: /// 1. heat /// 2. match /// 3. rank /// 4. score String attachSortParams(String input, String sort) { SearchParser parser = SearchParser(input); String newInput = parser.updateSort(sort); return newInput; } @action Future searchBangumi(String input, {String type = 'add'}) async { if (type != 'add') { bangumiList.clear(); bool privateMode = _collectRepository.getPrivateMode(); if (!privateMode) { // 检查是否已满,删除最旧的记录 if (_searchHistoryRepository.isHistoryFull(10)) { await _searchHistoryRepository.deleteOldest(); } // 删除重复的历史记录 await _searchHistoryRepository.deleteDuplicates(input); // 保存新的搜索历史 await _searchHistoryRepository.saveHistory(input); // 重新加载历史记录 loadSearchHistories(); } } isLoading = true; isTimeOut = false; SearchParser parser = SearchParser(input); String? idString = parser.parseId(); String? tag = parser.parseTag(); String? sort = parser.parseSort(); String keywords = parser.parseKeywords(); if (idString != null) { final id = int.tryParse(idString); if (id != null) { final BangumiItem? item = await BangumiHTTP.getBangumiInfoByID(id); if (item != null) { bangumiList.add(item); } return; } } var result = await BangumiHTTP.bangumiSearch(keywords, tags: [if (tag != null) tag], offset: bangumiList.length, sort: sort ?? 'heat'); bangumiList.addAll(result); isLoading = false; isTimeOut = bangumiList.isEmpty; } @action Future deleteSearchHistory(SearchHistory history) async { await _searchHistoryRepository.deleteHistory(history); loadSearchHistories(); } @action Future clearSearchHistory() async { await _searchHistoryRepository.clearAllHistories(); loadSearchHistories(); } @action Future setNotShowWatchedBangumis(bool value) async { notShowWatchedBangumis = value; await _collectRepository.updateSearchNotShowWatchedBangumis(value); } @action Future setNotShowAbandonedBangumis(bool value) async { notShowAbandonedBangumis = value; await _collectRepository.updateSearchNotShowAbandonedBangumis(value); } Set loadWatchedBangumiIds() { return _collectRepository.getBangumiIdsByType(CollectType.watched); } Set loadAbandonedBangumiIds() { return _collectRepository.getBangumiIdsByType(CollectType.abandoned); } } ================================================ FILE: lib/pages/search/search_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'search_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$SearchPageController on _SearchPageController, Store { late final _$isLoadingAtom = Atom(name: '_SearchPageController.isLoading', context: context); @override bool get isLoading { _$isLoadingAtom.reportRead(); return super.isLoading; } @override set isLoading(bool value) { _$isLoadingAtom.reportWrite(value, super.isLoading, () { super.isLoading = value; }); } late final _$isTimeOutAtom = Atom(name: '_SearchPageController.isTimeOut', context: context); @override bool get isTimeOut { _$isTimeOutAtom.reportRead(); return super.isTimeOut; } @override set isTimeOut(bool value) { _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () { super.isTimeOut = value; }); } late final _$notShowWatchedBangumisAtom = Atom( name: '_SearchPageController.notShowWatchedBangumis', context: context); @override bool get notShowWatchedBangumis { _$notShowWatchedBangumisAtom.reportRead(); return super.notShowWatchedBangumis; } bool _notShowWatchedBangumisIsInitialized = false; @override set notShowWatchedBangumis(bool value) { _$notShowWatchedBangumisAtom.reportWrite( value, _notShowWatchedBangumisIsInitialized ? super.notShowWatchedBangumis : null, () { super.notShowWatchedBangumis = value; _notShowWatchedBangumisIsInitialized = true; }); } late final _$notShowAbandonedBangumisAtom = Atom( name: '_SearchPageController.notShowAbandonedBangumis', context: context); @override bool get notShowAbandonedBangumis { _$notShowAbandonedBangumisAtom.reportRead(); return super.notShowAbandonedBangumis; } bool _notShowAbandonedBangumisIsInitialized = false; @override set notShowAbandonedBangumis(bool value) { _$notShowAbandonedBangumisAtom.reportWrite( value, _notShowAbandonedBangumisIsInitialized ? super.notShowAbandonedBangumis : null, () { super.notShowAbandonedBangumis = value; _notShowAbandonedBangumisIsInitialized = true; }); } late final _$bangumiListAtom = Atom(name: '_SearchPageController.bangumiList', context: context); @override ObservableList get bangumiList { _$bangumiListAtom.reportRead(); return super.bangumiList; } @override set bangumiList(ObservableList value) { _$bangumiListAtom.reportWrite(value, super.bangumiList, () { super.bangumiList = value; }); } late final _$searchHistoriesAtom = Atom(name: '_SearchPageController.searchHistories', context: context); @override ObservableList get searchHistories { _$searchHistoriesAtom.reportRead(); return super.searchHistories; } @override set searchHistories(ObservableList value) { _$searchHistoriesAtom.reportWrite(value, super.searchHistories, () { super.searchHistories = value; }); } late final _$searchBangumiAsyncAction = AsyncAction('_SearchPageController.searchBangumi', context: context); @override Future searchBangumi(String input, {String type = 'add'}) { return _$searchBangumiAsyncAction .run(() => super.searchBangumi(input, type: type)); } late final _$deleteSearchHistoryAsyncAction = AsyncAction( '_SearchPageController.deleteSearchHistory', context: context); @override Future deleteSearchHistory(SearchHistory history) { return _$deleteSearchHistoryAsyncAction .run(() => super.deleteSearchHistory(history)); } late final _$clearSearchHistoryAsyncAction = AsyncAction('_SearchPageController.clearSearchHistory', context: context); @override Future clearSearchHistory() { return _$clearSearchHistoryAsyncAction .run(() => super.clearSearchHistory()); } late final _$setNotShowWatchedBangumisAsyncAction = AsyncAction( '_SearchPageController.setNotShowWatchedBangumis', context: context); @override Future setNotShowWatchedBangumis(bool value) { return _$setNotShowWatchedBangumisAsyncAction .run(() => super.setNotShowWatchedBangumis(value)); } late final _$setNotShowAbandonedBangumisAsyncAction = AsyncAction( '_SearchPageController.setNotShowAbandonedBangumis', context: context); @override Future setNotShowAbandonedBangumis(bool value) { return _$setNotShowAbandonedBangumisAsyncAction .run(() => super.setNotShowAbandonedBangumis(value)); } late final _$_SearchPageControllerActionController = ActionController(name: '_SearchPageController', context: context); @override void loadSearchHistories() { final _$actionInfo = _$_SearchPageControllerActionController.startAction( name: '_SearchPageController.loadSearchHistories'); try { return super.loadSearchHistories(); } finally { _$_SearchPageControllerActionController.endAction(_$actionInfo); } } @override String toString() { return ''' isLoading: ${isLoading}, isTimeOut: ${isTimeOut}, notShowWatchedBangumis: ${notShowWatchedBangumis}, notShowAbandonedBangumis: ${notShowAbandonedBangumis}, bangumiList: ${bangumiList}, searchHistories: ${searchHistories} '''; } } ================================================ FILE: lib/pages/search/search_module.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/search/search_page.dart'; class SearchModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/:tag", child: (_) { return SearchPage(inputTag: r.args.params['tag']); }); } } ================================================ FILE: lib/pages/search/search_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/bean/card/bangumi_card.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; import 'package:kazumi/pages/search/search_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/utils/logger.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key, this.inputTag = ''}); final String inputTag; @override State createState() => _SearchPageState(); } class _SearchPageState extends State { final SearchController searchController = SearchController(); /// Don't use modular singleton here. We may have multiple search pages. /// Use a new instance of SearchPageController for each search page. final SearchPageController searchPageController = SearchPageController(); final ScrollController scrollController = ScrollController(); final List tabs = [ Tab(text: "排序方式"), Tab(text: "过滤器"), ]; @override void initState() { super.initState(); scrollController.addListener(scrollListener); searchPageController.loadSearchHistories(); } @override void dispose() { searchPageController.bangumiList.clear(); scrollController.removeListener(scrollListener); super.dispose(); } void scrollListener() { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200 && !searchPageController.isLoading && searchController.text != '' && searchPageController.bangumiList.length >= 20) { KazumiLogger().i('SearchController: search results is loading more'); searchPageController.searchBangumi(searchController.text, type: 'add'); } } Widget showFilterSwitcher() { return Wrap( children: [ Observer( builder: (context) => InkWell( onTap: () { searchPageController.setNotShowWatchedBangumis( !searchPageController.notShowWatchedBangumis); }, child: ListTile( title: const Text('不显示已看过的番剧'), trailing: Switch( value: searchPageController.notShowWatchedBangumis, onChanged: (value) { searchPageController.setNotShowWatchedBangumis(value); }, ), ), ), ), Observer( builder: (context) => InkWell( onTap: () { searchPageController.setNotShowAbandonedBangumis( !searchPageController.notShowAbandonedBangumis); }, child: ListTile( title: const Text('不显示已抛弃的番剧'), trailing: Switch( value: searchPageController.notShowAbandonedBangumis, onChanged: (value) { searchPageController.setNotShowAbandonedBangumis(value); }, ), ), ), ), ], ); } Widget showSortSwitcher() { return Wrap( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('按热度排序'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( searchController.text, 'heat'); searchPageController.searchBangumi(searchController.text, type: 'init'); }, ), ListTile( title: const Text('按评分排序'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( searchController.text, 'rank'); searchPageController.searchBangumi(searchController.text, type: 'init'); }, ), ListTile( title: const Text('按匹配程度排序'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( searchController.text, 'match'); searchPageController.searchBangumi(searchController.text, type: 'init'); }, ), ], ), ], ); } Widget showSearchOptionTabBar({required List options}) { return DefaultTabController( length: tabs.length, child: Scaffold( body: Column( children: [ PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Material( child: TabBar( tabs: tabs, ), ), ), Expanded( child: TabBarView( children: options, )) ], ))); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.inputTag != '') { final String tagString = 'tag:${Uri.decodeComponent(widget.inputTag)}'; searchController.text = tagString; searchPageController.searchBangumi(tagString, type: 'init'); } }); return Scaffold( appBar: SysAppBar( backgroundColor: Colors.transparent, title: const Text("搜索"), ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: (MediaQuery.sizeOf(context).height >= LayoutBreakpoint.compact['height']!) ? MediaQuery.of(context).size.height * 1 / 4 : MediaQuery.of(context).size.height, maxWidth: (MediaQuery.sizeOf(context).width >= LayoutBreakpoint.medium['width']!) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width, ), clipBehavior: Clip.antiAlias, backgroundColor: Theme.of(context).scaffoldBackgroundColor, context: context, builder: (context) { return showSearchOptionTabBar( options: [showSortSwitcher(), showFilterSwitcher()]); }, ); }, icon: const Icon(Icons.sort), label: const Text("搜索设置"), ), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 16), child: FocusScope( descendantsAreFocusable: false, child: SearchAnchor.bar( searchController: searchController, barElevation: WidgetStateProperty.fromMap( {WidgetState.any: 0}, ), viewElevation: 0, viewLeading: IconButton( onPressed: () { Navigator.of(context).pop(); }, icon: Icon(Icons.arrow_back), ), isFullScreen: MediaQuery.sizeOf(context).width < LayoutBreakpoint.compact['width']!, suggestionsBuilder: (context, controller) => [ Observer( builder: (context) { if (controller.text.isNotEmpty) { return Container( height: 400, alignment: Alignment.center, child: Text("无可用搜索建议,回车以直接检索"), ); } else { return Column( mainAxisSize: MainAxisSize.min, children: [ for (var history in searchPageController .searchHistories .take(10)) ListTile( title: Text(history.keyword), onTap: () { controller.text = history.keyword; searchPageController.searchBangumi( controller.text, type: 'init'); if (searchController.isOpen) { searchController.closeView(history.keyword); } }, trailing: IconButton( icon: const Icon(Icons.close), onPressed: () { searchPageController .deleteSearchHistory(history); }, ), ), ], ); } }, ), ], onSubmitted: (value) { searchPageController.searchBangumi(value, type: 'init'); if (searchController.isOpen) { searchController.closeView(value); } }, ), ), ), Expanded( child: Observer(builder: (context) { if (searchPageController.isTimeOut) { return Center( child: SizedBox( height: 400, child: GeneralErrorWidget( errMsg: '什么都没有找到 (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { searchPageController.searchBangumi( searchController.text, type: 'init'); }, text: '点击重试', ), ], ), ), ); } if (searchPageController.isLoading && searchPageController.bangumiList.isEmpty) { return Center(child: CircularProgressIndicator()); } int crossCount = 3; if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) { crossCount = 5; } if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) { crossCount = 6; } List filteredList = searchPageController.bangumiList.toList(); if (searchPageController.notShowWatchedBangumis) { final watchedBangumiIds = searchPageController.loadWatchedBangumiIds(); filteredList = filteredList .where((item) => !watchedBangumiIds.contains(item.id)) .toList(); } if (searchPageController.notShowAbandonedBangumis) { final abandonedBangumiIds = searchPageController.loadAbandonedBangumiIds(); filteredList = filteredList .where((item) => !abandonedBangumiIds.contains(item.id)) .toList(); } return GridView.builder( controller: scrollController, padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: StyleString.cardSpace - 2, crossAxisSpacing: StyleString.cardSpace, crossAxisCount: crossCount, mainAxisExtent: MediaQuery.of(context).size.width / crossCount / 0.65 + MediaQuery.textScalerOf(context).scale(32.0), ), itemCount: filteredList.isNotEmpty ? filteredList.length : 10, itemBuilder: (context, index) { return filteredList.isNotEmpty ? BangumiCardV( enableHero: false, bangumiItem: filteredList[index], ) : Container(); }, ); }), ), ], ), ); } } ================================================ FILE: lib/pages/settings/danmaku/danmaku_module.dart ================================================ import 'package:kazumi/pages/settings/danmaku/danmaku_settings.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/settings/danmaku/danmaku_shield_settings.dart'; class DanmakuModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/", child: (_) => const DanmakuSettingsPage()); r.child("/shield", child: (_) => const DanmakuShieldSettings()); } } ================================================ FILE: lib/pages/settings/danmaku/danmaku_settings.dart ================================================ import 'package:kazumi/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/pages/popular/popular_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class DanmakuSettingsPage extends StatefulWidget { const DanmakuSettingsPage({super.key}); @override State createState() => _DanmakuSettingsPageState(); } class _DanmakuSettingsPageState extends State { Box setting = GStorage.setting; late dynamic defaultDanmakuArea; late dynamic defaultDanmakuOpacity; late dynamic defaultDanmakuFontSize; late int defaultDanmakuFontWeight; late double defaultDanmakuDuration; late double defaultDanmakuLineHeight; late double defaultdanmakuBorderSize; final PopularController popularController = Modular.get(); late bool danmakuBorder; late bool danmakuTop; late bool danmakuBottom; late bool danmakuScroll; late bool danmakuColor; late bool danmakuMassive; late bool danmakuDeduplication; late bool danmakuBiliBiliSource; late bool danmakuGamerSource; late bool danmakuDanDanSource; late bool danmakuFollowSpeed; @override void initState() { super.initState(); defaultDanmakuArea = setting.get(SettingBoxKey.danmakuArea, defaultValue: 1.0); defaultDanmakuOpacity = setting.get(SettingBoxKey.danmakuOpacity, defaultValue: 1.0); defaultDanmakuFontSize = setting.get(SettingBoxKey.danmakuFontSize, defaultValue: (Utils.isCompact()) ? 16.0 : 25.0); defaultDanmakuFontWeight = setting.get(SettingBoxKey.danmakuFontWeight, defaultValue: 4); defaultDanmakuDuration = setting.get(SettingBoxKey.danmakuDuration, defaultValue: 8.0); defaultDanmakuLineHeight = setting.get(SettingBoxKey.danmakuLineHeight, defaultValue: 1.6); danmakuBorder = setting.get(SettingBoxKey.danmakuBorder, defaultValue: true); defaultdanmakuBorderSize = setting.get(SettingBoxKey.danmakuBorderSize, defaultValue: 1.5); danmakuTop = setting.get(SettingBoxKey.danmakuTop, defaultValue: true); danmakuBottom = setting.get(SettingBoxKey.danmakuBottom, defaultValue: false); danmakuScroll = setting.get(SettingBoxKey.danmakuScroll, defaultValue: true); danmakuColor = setting.get(SettingBoxKey.danmakuColor, defaultValue: true); danmakuMassive = setting.get(SettingBoxKey.danmakuMassive, defaultValue: false); danmakuDeduplication = setting.get(SettingBoxKey.danmakuDeduplication, defaultValue: false); danmakuBiliBiliSource = setting.get(SettingBoxKey.danmakuBiliBiliSource, defaultValue: true); danmakuGamerSource = setting.get(SettingBoxKey.danmakuGamerSource, defaultValue: true); danmakuDanDanSource = setting.get(SettingBoxKey.danmakuDanDanSource, defaultValue: true); danmakuFollowSpeed = setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } void updateDanmakuArea(double i) async { await setting.put(SettingBoxKey.danmakuArea, i); setState(() { defaultDanmakuArea = i; }); } void updateDanmakuOpacity(double i) async { await setting.put(SettingBoxKey.danmakuOpacity, i); setState(() { defaultDanmakuOpacity = i; }); } void updateDanmakuFontSize(double i) async { await setting.put(SettingBoxKey.danmakuFontSize, i); setState(() { defaultDanmakuFontSize = i; }); } void updateDanmakuDuration(double i) async { await setting.put(SettingBoxKey.danmakuDuration, i); setState(() { defaultDanmakuDuration = i; }); } void updateDanmakuLineHeight(double i) async { await setting.put(SettingBoxKey.danmakuLineHeight, i); setState(() { defaultDanmakuLineHeight = i; }); } void updateDanmakuFontWeight(int i) async { await setting.put(SettingBoxKey.danmakuFontWeight, i); setState(() { defaultDanmakuFontWeight = i; }); } void updateDanmakuBorderSize(double i) async { await setting.put(SettingBoxKey.danmakuBorderSize, i); setState(() { defaultdanmakuBorderSize = i; }); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('弹幕设置')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { danmakuBiliBiliSource = value ?? !danmakuBiliBiliSource; await setting.put(SettingBoxKey.danmakuBiliBiliSource, danmakuBiliBiliSource); setState(() {}); }, title: Text('BiliBili', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuBiliBiliSource, ), SettingsTile.switchTile( onToggle: (value) async { danmakuGamerSource = value ?? !danmakuGamerSource; await setting.put( SettingBoxKey.danmakuGamerSource, danmakuGamerSource); setState(() {}); }, title: Text('Gamer', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuGamerSource, ), SettingsTile.switchTile( onToggle: (value) async { danmakuDanDanSource = value ?? !danmakuDanDanSource; await setting.put( SettingBoxKey.danmakuDanDanSource, danmakuDanDanSource); setState(() {}); }, title: Text('DanDan', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuDanDanSource, ), ], ), SettingsSection( title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/danmaku/shield'); }, title: Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuArea, min: 0, max: 1, divisions: 8, label: '${(defaultDanmakuArea * 100).round()}%', onChanged: (value) { updateDanmakuArea(value); }, ), ), SettingsTile( title: Text('弹幕持续时间', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuDuration, min: 2, max: 16, divisions: 14, label: '${defaultDanmakuDuration.round()}', onChanged: (value) { updateDanmakuDuration(value.round().toDouble()); }, ), ), SettingsTile( title: Text('弹幕行高', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuLineHeight, min: 0, max: 3, divisions: 30, label: defaultDanmakuLineHeight.toStringAsFixed(1), onChanged: (value) { updateDanmakuLineHeight(double.parse(value.toStringAsFixed(1))); }, ), ), SettingsTile.switchTile( onToggle: (value) async { danmakuFollowSpeed = value ?? !danmakuFollowSpeed; await setting.put( SettingBoxKey.danmakuFollowSpeed, danmakuFollowSpeed); setState(() {}); }, title: Text('弹幕跟随视频倍速', style: TextStyle(fontFamily: fontFamily)), description: Text('开启后弹幕速度会随视频倍速而改变', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuFollowSpeed, ), SettingsTile.switchTile( onToggle: (value) async { danmakuTop = value ?? !danmakuTop; await setting.put(SettingBoxKey.danmakuTop, danmakuTop); setState(() {}); }, title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuTop, ), SettingsTile.switchTile( onToggle: (value) async { danmakuBottom = value ?? !danmakuBottom; await setting.put( SettingBoxKey.danmakuBottom, danmakuBottom); setState(() {}); }, title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuBottom, ), SettingsTile.switchTile( onToggle: (value) async { danmakuScroll = value ?? !danmakuScroll; await setting.put( SettingBoxKey.danmakuScroll, danmakuScroll); setState(() {}); }, title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuScroll, ), SettingsTile.switchTile( onToggle: (value) async { danmakuMassive = value ?? !danmakuMassive; await setting.put( SettingBoxKey.danmakuMassive, danmakuMassive); setState(() {}); }, title: Text('海量弹幕', style: TextStyle(fontFamily: fontFamily)), description: Text('弹幕过多时进行叠加绘制', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuMassive, ), SettingsTile.switchTile( onToggle: (value) async { danmakuDeduplication = value ?? !danmakuDeduplication; await setting.put( SettingBoxKey.danmakuDeduplication, danmakuDeduplication); setState(() {}); }, title: Text('弹幕去重', style: TextStyle(fontFamily: fontFamily)), description: Text('相同内容弹幕过多时合并为一条弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuDeduplication, ), ], ), SettingsSection( title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { danmakuBorder = value ?? !danmakuBorder; await setting.put( SettingBoxKey.danmakuBorder, danmakuBorder); setState(() {}); }, title: Text('弹幕描边', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuBorder, ), SettingsTile( title: Text('弹幕描边粗细', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultdanmakuBorderSize, min: 0.1, max: 3, divisions: 29, label: defaultdanmakuBorderSize.toStringAsFixed(1), onChanged: (value) { updateDanmakuBorderSize(double.parse(value.toStringAsFixed(1))); }, ), ), SettingsTile.switchTile( onToggle: (value) async { danmakuColor = value ?? !danmakuColor; await setting.put(SettingBoxKey.danmakuColor, danmakuColor); setState(() {}); }, title: Text('弹幕颜色', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuColor, ), SettingsTile( title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuFontSize, min: 10, max: Utils.isCompact() ? 32 : 48, label: '${defaultDanmakuFontSize.floorToDouble()}', onChanged: (value) { updateDanmakuFontSize(value.floorToDouble()); }, ), ), SettingsTile( title: Text('字体字重', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuFontWeight.toDouble(), min: 1, max: 9, divisions: 8, label: '$defaultDanmakuFontWeight', onChanged: (value) { updateDanmakuFontWeight(value.toInt()); }, ), ), SettingsTile( title: Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuOpacity, min: 0.1, max: 1, label: '${(defaultDanmakuOpacity * 100).round()}%', onChanged: (value) { updateDanmakuOpacity( double.parse(value.toStringAsFixed(2))); }, ), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/settings/danmaku/danmaku_settings_sheet.dart ================================================ import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/pages/settings/danmaku/danmaku_shield_settings.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class DanmakuSettingsSheet extends StatefulWidget { final DanmakuController danmakuController; final VoidCallback? onUpdateDanmakuSpeed; const DanmakuSettingsSheet({ super.key, required this.danmakuController, this.onUpdateDanmakuSpeed, }); @override State createState() => _DanmakuSettingsSheetState(); } class _DanmakuSettingsSheetState extends State { Box setting = GStorage.setting; void showDanmakuShieldSheet() { showModalBottomSheet( isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, maxWidth: (Utils.isDesktop() || Utils.isTablet()) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return SafeArea( bottom: false, child: DanmakuShieldSettings(), ); }); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return SafeArea( bottom: false, child: SettingsList( sections: [ SettingsSection( title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { showDanmakuShieldSheet(); }, title: Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.fontSize, min: 10, max: Utils.isCompact() ? 32 : 48, label: '${widget.danmakuController.option.fontSize.floorToDouble()}', onChanged: (value) { setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( fontSize: value.floorToDouble(), ), )); setting.put( SettingBoxKey.danmakuFontSize, value.floorToDouble()); }, ), ), SettingsTile( title: Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.opacity, min: 0.1, max: 1, label: '${(widget.danmakuController.option.opacity * 100).round()}%', onChanged: (value) { setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( opacity: value, ), )); setting.put(SettingBoxKey.danmakuOpacity, double.parse(value.toStringAsFixed(2))); }, ), ), ], ), SettingsSection( title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.area, min: 0, max: 1, divisions: 8, label: '${(widget.danmakuController.option.area * 100).round()}%', onChanged: (value) { setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( area: value, ), )); setting.put(SettingBoxKey.danmakuArea, value); }, ), ), SettingsTile(title: Text('持续时间', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.duration.toDouble(), min: 2, max: 16, divisions: 14, label: '${widget.danmakuController.option.duration.round()}', onChanged: (value) { setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( duration: value, ), )); setting.put(SettingBoxKey.danmakuDuration, value.round().toDouble()); }, ), ), SettingsTile( title: Text('行高', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.lineHeight, min: 0, max: 3, divisions: 30, label: widget.danmakuController.option.lineHeight.toStringAsFixed(1), onChanged: (value) { setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( lineHeight: double.parse(value.toStringAsFixed(1)), ), )); setting.put(SettingBoxKey.danmakuLineHeight, double.parse(value.toStringAsFixed(1))); }, ), ), SettingsTile.switchTile( onToggle: (value) async { bool show = value ?? widget.danmakuController.option.hideTop; setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( hideTop: !show, ), )); setting.put(SettingBoxKey.danmakuTop, show); }, title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideTop, ), SettingsTile.switchTile( onToggle: (value) async { bool show = value ?? widget.danmakuController.option.hideBottom; setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( hideBottom: !show, ), )); setting.put(SettingBoxKey.danmakuBottom, show); }, title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideBottom, ), SettingsTile.switchTile( onToggle: (value) async { bool show = value ?? widget.danmakuController.option.hideScroll; setState(() => widget.danmakuController.updateOption( widget.danmakuController.option.copyWith( hideScroll: !show, ), )); setting.put(SettingBoxKey.danmakuScroll, show); }, title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideScroll, ), SettingsTile.switchTile( onToggle: (value) async { bool followSpeed = value ?? !setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true); setting.put(SettingBoxKey.danmakuFollowSpeed, followSpeed); widget.onUpdateDanmakuSpeed?.call(); setState(() {}); }, title: Text('跟随视频倍速', style: TextStyle(fontFamily: fontFamily)), description: Text('弹幕速度随视频倍速变化', style: TextStyle(fontFamily: fontFamily)), initialValue: setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true), ), ], ), ], ), ); } } ================================================ FILE: lib/pages/settings/danmaku/danmaku_shield_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/pages/my/my_controller.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; class DanmakuShieldSettings extends StatefulWidget { const DanmakuShieldSettings({super.key}); @override State createState() => _DanmakuShieldSettingsState(); } class _DanmakuShieldSettingsState extends State { final MyController myController = Modular.get(); final TextEditingController textEditingController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: SysAppBar( title: const Text("弹幕屏蔽"), ), body: ListView( padding: EdgeInsets.all(12), children: [ TextField( controller: textEditingController, decoration: InputDecoration( border: const OutlineInputBorder(), hintText: "输入关键词或正则表达式", suffixIcon: TextButton.icon( onPressed: () { myController.addShieldList( textEditingController.text.trim(), ); }, icon: const Icon(Icons.add), label: const Text("添加"), ), ), onSubmitted: (_) { myController.addShieldList( textEditingController.text.trim(), ); }, ), SizedBox(height: 12), Text( '以"/"开头和结尾将视作正则表达式, 如"/\\d+/"表示屏蔽所有数字', ), Observer(builder: (context) { return Text( "已添加${myController.shieldList.length}个关键词", ); }), SizedBox(height: 12), Observer(builder: (context) { return Wrap( runSpacing: 12, spacing: 12, children: myController.shieldList .map( (item) => Chip( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(32)), backgroundColor: Theme.of(context).colorScheme.secondaryContainer, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, side: BorderSide.none, label: Text( item, style: TextStyle( fontSize: 14, ), ), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), deleteIcon: Icon(Icons.close, size: 18), deleteButtonTooltipMessage: '', onDeleted: () { myController.removeShieldList(item); }, ), ) .toList(), ); }) ], ), ); } } ================================================ FILE: lib/pages/settings/decoder_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class DecoderSettings extends StatefulWidget { const DecoderSettings({super.key}); @override State createState() => _DecoderSettingsState(); } class _DecoderSettingsState extends State { late final Box setting = GStorage.setting; late final ValueNotifier decoder = ValueNotifier( setting.get(SettingBoxKey.hardwareDecoder, defaultValue: 'auto-safe'), ); @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( title: Text('硬件解码器'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('选择不受支持的解码器将回退到软件解码', style: TextStyle(fontFamily: fontFamily)), tiles: hardwareDecodersList.entries .map((e) => SettingsTile.radioTile( title: Text(e.key, style: TextStyle(fontFamily: fontFamily)), description: Text(e.value, style: TextStyle(fontFamily: fontFamily)), radioValue: e.key, groupValue: decoder.value, onChanged: (String? value) { if (value != null) { setting.put(SettingBoxKey.hardwareDecoder, value); setState(() { decoder.value = value; }); } }, )) .toList(), ), ], ), ); } } ================================================ FILE: lib/pages/settings/displaymode_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class SetDisplayMode extends StatefulWidget { const SetDisplayMode({super.key}); @override State createState() => _SetDisplayModeState(); } class _SetDisplayModeState extends State { List modes = []; DisplayMode? active; DisplayMode? preferred; Box setting = GStorage.setting; final ValueNotifier page = ValueNotifier(0); late final PageController controller = PageController() ..addListener(() { page.value = controller.page!.round(); }); @override void initState() { super.initState(); init(); SchedulerBinding.instance.addPostFrameCallback((_) { fetchAll(); }); } Future fetchAll() async { preferred = await FlutterDisplayMode.preferred; active = await FlutterDisplayMode.active; await setting.put(SettingBoxKey.displayMode, preferred.toString()); setState(() {}); } Future init() async { try { modes = await FlutterDisplayMode.supported; } on PlatformException catch (_) {} var res = await getDisplayModeType(modes); preferred = modes.toList().firstWhere((el) => el == res); FlutterDisplayMode.setPreferredMode(preferred!); } Future getDisplayModeType(modes) async { var value = setting.get(SettingBoxKey.displayMode); DisplayMode f = DisplayMode.auto; if (value != null) { f = modes.firstWhere((e) => e.toString() == value); } return f; } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: AppBar(title: const Text('屏幕帧率设置')), body: (modes.isEmpty) ? const CircularProgressIndicator() : SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('没有生效? 重启app试试', style: TextStyle(fontFamily: fontFamily)), tiles: modes .map((e) => SettingsTile.radioTile( radioValue: e, groupValue: preferred, onChanged: (DisplayMode? newMode) async { await FlutterDisplayMode.setPreferredMode( newMode!); await Future.delayed( const Duration(milliseconds: 100), ); await fetchAll(); }, title: e == DisplayMode.auto ? Text('自动', style: TextStyle(fontFamily: fontFamily)) : Text('$e${e == active ? " [系统]" : ""}', style: TextStyle(fontFamily: fontFamily)), )) .toList(), ), ], ), ); } } ================================================ FILE: lib/pages/settings/download_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class DownloadSettingsPage extends StatefulWidget { const DownloadSettingsPage({super.key}); @override State createState() => _DownloadSettingsPageState(); } class _DownloadSettingsPageState extends State { Box setting = GStorage.setting; late int parallelEpisodes; late int parallelSegments; late bool downloadDanmaku; @override void initState() { super.initState(); parallelEpisodes = setting.get( SettingBoxKey.downloadParallelEpisodes, defaultValue: 2, ); parallelSegments = setting.get( SettingBoxKey.downloadParallelSegments, defaultValue: 3, ); downloadDanmaku = setting.get( SettingBoxKey.downloadDanmaku, defaultValue: true, ); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar(title: Text('下载设置')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('并发设置', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( title: Text('同时下载集数', style: TextStyle(fontFamily: fontFamily)), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '同时下载 $parallelEpisodes 集', style: TextStyle(fontFamily: fontFamily), ), Slider( value: parallelEpisodes.toDouble(), min: 1, max: 5, divisions: 4, label: '$parallelEpisodes', onChanged: (value) { setState(() => parallelEpisodes = value.toInt()); setting.put( SettingBoxKey.downloadParallelEpisodes, parallelEpisodes, ); }, ), ], ), ), SettingsTile( title: Text('分片并发数', style: TextStyle(fontFamily: fontFamily)), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '每集同时下载 $parallelSegments 个分片', style: TextStyle(fontFamily: fontFamily), ), Slider( value: parallelSegments.toDouble(), min: 1, max: 10, divisions: 9, label: '$parallelSegments', onChanged: (value) { setState(() => parallelSegments = value.toInt()); setting.put( SettingBoxKey.downloadParallelSegments, parallelSegments, ); }, ), ], ), ), ], ), SettingsSection( title: Text('缓存设置', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) { setState(() => downloadDanmaku = value ?? !downloadDanmaku); setting.put(SettingBoxKey.downloadDanmaku, downloadDanmaku); }, title: Text('缓存弹幕', style: TextStyle(fontFamily: fontFamily)), description: Text( '下载视频时同时缓存弹幕数据', style: TextStyle(fontFamily: fontFamily), ), initialValue: downloadDanmaku, ), ], ), SettingsSection( title: Text('说明', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( title: Text('关于并发设置', style: TextStyle(fontFamily: fontFamily)), description: Text( '• 集数并发:同时下载多少集视频\n' '• 分片并发:每集内同时下载多少个视频片段\n' '• 较高的并发可提升速度,但可能被服务器限制\n' '• 修改后对新开始的下载生效', style: TextStyle(fontFamily: fontFamily), ), ), ], ), ], ), ); } } ================================================ FILE: lib/pages/settings/interface_settings.dart ================================================ import 'package:card_settings_ui/list/settings_list.dart'; import 'package:card_settings_ui/section/settings_section.dart'; import 'package:card_settings_ui/tile/settings_tile.dart'; import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; class InterfaceSettingsPage extends StatefulWidget { const InterfaceSettingsPage({super.key}); @override State createState() => _InterfaceSettingsPageState(); } class _InterfaceSettingsPageState extends State { Box setting = GStorage.setting; late bool showRating; late String defaultPage; final MenuController defaultPageMenuController = MenuController(); static const Map defaultPageMap = { '/tab/popular/': '推荐', '/tab/timeline/': '时间表', '/tab/collect/': '追番', '/tab/my/': '我的', }; @override void initState() { super.initState(); showRating = setting.get(SettingBoxKey.showRating, defaultValue: true); defaultPage = setting.get(SettingBoxKey.defaultStartupPage, defaultValue: '/tab/popular/'); } void updateDefaultPage(String page) { setting.put(SettingBoxKey.defaultStartupPage, page); setState(() { defaultPage = page; }); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: SysAppBar( title: Text('界面设置'), ), body: SettingsList( sections: [ SettingsSection(tiles: [ SettingsTile.navigation( onPressed: (_) async { if (defaultPageMenuController.isOpen) { defaultPageMenuController.close(); } else { defaultPageMenuController.open(); } }, title: Text('启动界面设置', style: TextStyle(fontFamily: fontFamily)), description: Text('设置应用开启时的默认页面', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: defaultPageMenuController, builder: (_, __, ___) { return Text( defaultPageMap[defaultPage] ?? '推荐', style: TextStyle(fontFamily: fontFamily), ); }, menuChildren: [ for (final entry in defaultPageMap.entries) MenuItemButton( requestFocusOnHover: false, onPressed: () => updateDefaultPage(entry.key), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( entry.value, style: TextStyle( color: entry.key == defaultPage ? Theme.of(context).colorScheme.primary : null, fontFamily: fontFamily, ), ), ), ), ), ], ), ), ]), SettingsSection(tiles: [ SettingsTile.switchTile( onToggle: (value) async { showRating = value ?? !showRating; await setting.put(SettingBoxKey.showRating, showRating); setState(() {}); }, title: Text('显示评分', style: TextStyle(fontFamily: fontFamily)), description: Text('关闭后将在概览中隐藏评分信息', style: TextStyle(fontFamily: fontFamily)), initialValue: showRating, ), ]), ], ), ); } } ================================================ FILE: lib/pages/settings/keyboard_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; class KeyboardSettingsPage extends StatefulWidget { const KeyboardSettingsPage({super.key}); @override State createState() => _KeyboardSettingsPageState(); } class _KeyboardSettingsPageState extends State { Box setting = GStorage.setting; String? listeningFunction; int? listeningIndex; late Map> shortcuts; final FocusNode focusNode = FocusNode(); @override void initState() { super.initState(); // 根据默认快捷键生成可用快捷键列表,并读取已设置值 shortcuts = { for (var key in defaultShortcuts.keys) key: (setting.get('shortcut_$key', defaultValue: defaultShortcuts[key]?.toList() ?? []) ?.cast() ?? []) }; } @override void dispose() { // 清理空的快捷键设置 for (final entry in shortcuts.entries) { final func = entry.key; final keys = entry.value; keys.removeWhere((key) => key.isEmpty || key == '...'); setting.put('shortcut_$func', keys); } focusNode.dispose(); super.dispose(); } bool handleShortcutInput(String rawKey) { if (listeningFunction == null || listeningIndex == null) return false; final func = listeningFunction!; final index = listeningIndex!; // 冲突规避 for (final entry in shortcuts.entries) { final otherFunc = entry.key; final otherKeys = entry.value; for (int i = 0; i < otherKeys.length; i++) { if (otherFunc == func && i == index) continue; if (otherKeys[i] == rawKey) { final name = shortcutsChineseName[otherFunc] ?? otherFunc; KazumiDialog.showToast(message: "按键已被【$name】占用,请重新输入"); return true; } } } setState(() { shortcuts[func]![index] = rawKey; listeningFunction = null; listeningIndex = null; }); setting.put('shortcut_$func', shortcuts[func]); return true; } void startListening(String func, int index) { setState(() { listeningFunction = func; listeningIndex = index; shortcuts[func]![index] = '...'; }); Future.delayed(const Duration(milliseconds: 50), () { focusNode.requestFocus(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: SysAppBar( title: Text('快捷键'), actions: [ IconButton( icon: const Icon(Icons.refresh), tooltip: '恢复默认', onPressed: () { setState(() { for (final func in shortcuts.keys) { shortcuts[func] = defaultShortcuts[func]?.toList() ?? []; setting.put('shortcut_$func', shortcuts[func]); } }); }, ), ], ), body: FocusScope( autofocus: true, child: Focus( focusNode: focusNode, autofocus: true, canRequestFocus: true, skipTraversal: true, descendantsAreFocusable: true, onKeyEvent: (node, event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; if (listeningFunction == null) return KeyEventResult.ignored; final rawKey = event.logicalKey.keyLabel.isNotEmpty ? event.logicalKey.keyLabel : event.logicalKey.debugName ?? ''; final handled = handleShortcutInput(rawKey); return handled ? KeyEventResult.handled : KeyEventResult.ignored; }, child: ListView( padding: const EdgeInsets.all(16), children: shortcuts.entries.map((entry) { final func = entry.key; final keys = entry.value; return Card( margin: const EdgeInsets.symmetric(vertical: 8), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( shortcutsChineseName[func] ?? func, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold) ), Spacer(), IconButton( icon: Icon(Icons.add), onPressed: () { keys.removeWhere((key) => key.isEmpty || key == '...'); setState(() => keys.add('')); setting.put('shortcut_$func', keys); startListening(func, keys.length - 1); }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), focusNode: FocusNode(canRequestFocus: false), ), ], ), if (keys.isNotEmpty) const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ for (int i = 0; i < keys.length; i++) ActionChip( label: Text(keyAliases[keys[i]] ?? keys[i],), avatar: keys.length >=2 ?Icon(Icons.cancel) :Icon(Icons.edit), onPressed: (keys.length >=2) ?() { setState(() { keys.removeAt(i); listeningIndex = null; if (keys.length >1){ keys.removeWhere((key) => key.isEmpty || key == '...'); } setting.put('shortcut_$func', keys); }); } :() => startListening(func, 0), focusNode: FocusNode(canRequestFocus: false), ), ], ), ], ), ), ); }).toList(), ), ), ), ); } } ================================================ FILE: lib/pages/settings/player_settings.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class PlayerSettingsPage extends StatefulWidget { const PlayerSettingsPage({super.key}); @override State createState() => _PlayerSettingsPageState(); } class _PlayerSettingsPageState extends State { Box setting = GStorage.setting; late double defaultPlaySpeed; late double defaultShortcutForwardPlaySpeed; late int defaultAspectRatioType; late bool hAenable; late bool androidEnableOpenSLES; late bool lowMemoryMode; late bool playResume; late bool showPlayerError; late bool privateMode; late bool playerDebugMode; late bool playerDisableAnimations; late bool forceAdBlocker; late bool autoPlayNext; late int playerButtonSkipTime; late int playerArrowKeySkipTime; late int playerLogLevel; final MenuController playerAspectRatioMenuController = MenuController(); final MenuController playerLogLevelMenuController = MenuController(); @override void initState() { super.initState(); defaultPlaySpeed = setting.get(SettingBoxKey.defaultPlaySpeed, defaultValue: 1.0); defaultShortcutForwardPlaySpeed = setting.get(SettingBoxKey.defaultShortcutForwardPlaySpeed, defaultValue: 2.0); defaultAspectRatioType = setting.get(SettingBoxKey.defaultAspectRatioType, defaultValue: 1); hAenable = setting.get(SettingBoxKey.hAenable, defaultValue: true); androidEnableOpenSLES = setting.get(SettingBoxKey.androidEnableOpenSLES, defaultValue: true); lowMemoryMode = setting.get(SettingBoxKey.lowMemoryMode, defaultValue: false); playResume = setting.get(SettingBoxKey.playResume, defaultValue: true); privateMode = setting.get(SettingBoxKey.privateMode, defaultValue: false); showPlayerError = setting.get(SettingBoxKey.showPlayerError, defaultValue: true); playerDebugMode = setting.get(SettingBoxKey.playerDebugMode, defaultValue: false); autoPlayNext = setting.get(SettingBoxKey.autoPlayNext, defaultValue: true); playerDisableAnimations = setting.get(SettingBoxKey.playerDisableAnimations, defaultValue: false); forceAdBlocker = setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false); playerLogLevel = setting.get(SettingBoxKey.playerLogLevel, defaultValue: 2); playerButtonSkipTime = setting.get(SettingBoxKey.buttonSkipTime, defaultValue: 80); playerArrowKeySkipTime = setting.get(SettingBoxKey.arrowKeySkipTime, defaultValue: 10); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } void updateDefaultPlaySpeed(double speed) { setting.put(SettingBoxKey.defaultPlaySpeed, speed); setState(() { defaultPlaySpeed = speed; }); } void updateDefaultShortcutForwardPlaySpeed(double speed) { setting.put(SettingBoxKey.defaultShortcutForwardPlaySpeed, speed); setState(() { defaultShortcutForwardPlaySpeed = speed; }); } void updatePlayerLogLevel(int level) { setting.put(SettingBoxKey.playerLogLevel, level); setState(() { playerLogLevel = level; }); } void updateDefaultAspectRatioType(int type) { setting.put(SettingBoxKey.defaultAspectRatioType, type); setState(() { defaultAspectRatioType = type; }); } Future updateButtonSkipTime() async { final int? newButtonSkipTime = await _showSkipTimeChangeDialog( title: '顶部按钮快进时长', initialValue: playerButtonSkipTime.toString()); print('新设置的顶部按钮快进时长: $newButtonSkipTime'); if (newButtonSkipTime != null && newButtonSkipTime != playerButtonSkipTime) { setting.put(SettingBoxKey.buttonSkipTime, newButtonSkipTime); setState(() { playerButtonSkipTime = newButtonSkipTime; }); } } Future _showSkipTimeChangeDialog( {required String title, required String initialValue}) async { return KazumiDialog.show(builder: (context) { String input = ""; return AlertDialog( title: Text(title), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // 只允许输入数字 ], decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, // 控制label的显示方式 labelText: initialValue, ), onChanged: (value) { input = value; }, ); }), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { final int? newValue = int.tryParse(input); if (newValue == null) { KazumiDialog.showToast(message: '请输入数字'); return; } if (newValue <= 0) { KazumiDialog.showToast(message: '请输入大于0的数字'); return; } // 以新设置的值弹出 KazumiDialog.dismiss(popWith: newValue); }, child: const Text('确定'), ), ], ); }); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('播放设置')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( tiles: [ SettingsTile.switchTile( onToggle: (value) async { hAenable = value ?? !hAenable; await setting.put(SettingBoxKey.hAenable, hAenable); setState(() {}); }, title: Text('硬件解码', style: TextStyle(fontFamily: fontFamily)), initialValue: hAenable, ), SettingsTile.navigation( onPressed: (_) async { await Modular.to.pushNamed('/settings/player/decoder'); }, title: Text('硬件解码器', style: TextStyle(fontFamily: fontFamily)), description: Text('仅在硬件解码启用时生效', style: TextStyle(fontFamily: fontFamily)), ), if (Platform.isAndroid) ...[ SettingsTile.navigation( onPressed: (_) async { await Modular.to.pushNamed('/settings/player/renderer'); }, title: Text('视频渲染器', style: TextStyle(fontFamily: fontFamily)), description: Text('选择视频输出方式', style: TextStyle(fontFamily: fontFamily)), ), ], SettingsTile.switchTile( onToggle: (value) async { lowMemoryMode = value ?? !lowMemoryMode; await setting.put( SettingBoxKey.lowMemoryMode, lowMemoryMode); setState(() {}); }, title: Text('低内存模式', style: TextStyle(fontFamily: fontFamily)), description: Text('禁用高级缓存以减少内存占用', style: TextStyle(fontFamily: fontFamily)), initialValue: lowMemoryMode, ), if (Platform.isAndroid) ...[ SettingsTile.switchTile( onToggle: (value) async { androidEnableOpenSLES = value ?? !androidEnableOpenSLES; await setting.put(SettingBoxKey.androidEnableOpenSLES, androidEnableOpenSLES); setState(() {}); }, title: Text('低延迟音频', style: TextStyle(fontFamily: fontFamily)), description: Text('启用OpenSLES音频输出以降低延时', style: TextStyle(fontFamily: fontFamily)), initialValue: androidEnableOpenSLES, ), ], SettingsTile.navigation( onPressed: (_) async { Modular.to.pushNamed('/settings/player/super'); }, title: Text('超分辨率', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( tiles: [ SettingsTile.switchTile( onToggle: (value) async { playResume = value ?? !playResume; await setting.put(SettingBoxKey.playResume, playResume); setState(() {}); }, title: Text('自动跳转', style: TextStyle(fontFamily: fontFamily)), description: Text('跳转到上次播放位置', style: TextStyle(fontFamily: fontFamily)), initialValue: playResume, ), SettingsTile.switchTile( onToggle: (value) async { autoPlayNext = value ?? !autoPlayNext; await setting.put(SettingBoxKey.autoPlayNext, autoPlayNext); setState(() {}); }, title: Text('自动连播', style: TextStyle(fontFamily: fontFamily)), description: Text('当前视频播放完毕后自动播放下一集', style: TextStyle(fontFamily: fontFamily)), initialValue: autoPlayNext, ), SettingsTile.switchTile( onToggle: (value) async { forceAdBlocker = value ?? !forceAdBlocker; await setting.put(SettingBoxKey.forceAdBlocker, forceAdBlocker); setState(() {}); }, title: Text('广告过滤', style: TextStyle(fontFamily: fontFamily)), description: Text('强制启用HLS广告过滤,忽略规则设置', style: TextStyle(fontFamily: fontFamily)), initialValue: forceAdBlocker, ), SettingsTile.switchTile( onToggle: (value) async { playerDisableAnimations = value ?? !playerDisableAnimations; await setting.put(SettingBoxKey.playerDisableAnimations, playerDisableAnimations); setState(() {}); }, title: Text('禁用动画', style: TextStyle(fontFamily: fontFamily)), description: Text('禁用播放器内的过渡动画', style: TextStyle(fontFamily: fontFamily)), initialValue: playerDisableAnimations, ), SettingsTile.switchTile( onToggle: (value) async { privateMode = value ?? !privateMode; await setting.put(SettingBoxKey.privateMode, privateMode); setState(() {}); }, title: Text('隐身模式', style: TextStyle(fontFamily: fontFamily)), description: Text('不保留观看记录', style: TextStyle(fontFamily: fontFamily)), initialValue: privateMode, ), ], ), SettingsSection( tiles: [ SettingsTile.switchTile( onToggle: (value) async { showPlayerError = value ?? !showPlayerError; await setting.put( SettingBoxKey.showPlayerError, showPlayerError); setState(() {}); }, title: Text('错误提示', style: TextStyle(fontFamily: fontFamily)), description: Text('显示播放器内部错误提示', style: TextStyle(fontFamily: fontFamily)), initialValue: showPlayerError, ), SettingsTile.switchTile( onToggle: (value) async { playerDebugMode = value ?? !playerDebugMode; await setting.put( SettingBoxKey.playerDebugMode, playerDebugMode); setState(() {}); }, title: Text('调试模式', style: TextStyle(fontFamily: fontFamily)), description: Text('记录播放器内部日志', style: TextStyle(fontFamily: fontFamily)), initialValue: playerDebugMode, ), SettingsTile.navigation( onPressed: (_) async { if (playerLogLevelMenuController.isOpen) { playerLogLevelMenuController.close(); } else { playerLogLevelMenuController.open(); } }, title: Text('日志等级', style: TextStyle(fontFamily: fontFamily)), description: Text('播放器内部日志等级', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: playerLogLevelMenuController, builder: (_, __, ___) { return Text( playerLogLevelMap[playerLogLevel] ?? '???', ); }, menuChildren: [ for (final entry in playerLogLevelMap.entries) MenuItemButton( requestFocusOnHover: false, onPressed: () => updatePlayerLogLevel(entry.key), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( entry.value, style: TextStyle( color: entry.key == playerLogLevel ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ], ), ), ], ), SettingsSection( tiles: [ SettingsTile( title: Text('默认倍速', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultPlaySpeed, min: 0.25, max: 3, divisions: 11, label: '${defaultPlaySpeed}x', onChanged: (value) { updateDefaultPlaySpeed( double.parse(value.toStringAsFixed(2))); }, ), ), SettingsTile( title: Text('默认方向键倍速', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultShortcutForwardPlaySpeed, min: 1.25, max: 3, divisions: 7, label: '${defaultShortcutForwardPlaySpeed}x', onChanged: (value) { updateDefaultShortcutForwardPlaySpeed( double.parse(value.toStringAsFixed(2))); }, ), ), SettingsTile.navigation( description: Slider( value: playerArrowKeySkipTime.toDouble(), min: 0, max: 15, divisions: 15, label: '$playerArrowKeySkipTime秒', onChanged: (value) { final newArrowKeySkipTime = value.toInt(); print('新设置的方向键快进/快退时长: $newArrowKeySkipTime'); if (value != playerArrowKeySkipTime) { setting.put(SettingBoxKey.arrowKeySkipTime, newArrowKeySkipTime); setState(() { playerArrowKeySkipTime = newArrowKeySkipTime; }); } }, ), title: Text('左右方向键的快进/快退秒数', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) async { await updateButtonSkipTime(); }, title: Text('跳过时长', style: TextStyle(fontFamily: fontFamily)), description: Text('顶栏跳过按钮的秒数', style: TextStyle(fontFamily: fontFamily)), value: Text('$playerButtonSkipTime 秒', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) async { if (playerAspectRatioMenuController.isOpen) { playerAspectRatioMenuController.close(); } else { playerAspectRatioMenuController.open(); } }, title: Text('默认视频比例', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: playerAspectRatioMenuController, builder: (_, __, ___) { return Text( aspectRatioTypeMap[defaultAspectRatioType] ?? '自动', style: TextStyle(fontFamily: fontFamily), ); }, menuChildren: [ for (final entry in aspectRatioTypeMap.entries) MenuItemButton( requestFocusOnHover: false, onPressed: () => updateDefaultAspectRatioType(entry.key), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( entry.value, style: TextStyle( color: entry.key == defaultAspectRatioType ? Theme.of(context).colorScheme.primary : null, fontFamily: fontFamily, ), ), ), ), ), ], ), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/settings/proxy/proxy_editor_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/proxy_manager.dart'; import 'package:kazumi/request/request.dart'; class ProxyEditorPage extends StatefulWidget { const ProxyEditorPage({super.key}); @override State createState() => _ProxyEditorPageState(); } class _ProxyEditorPageState extends State { Box setting = GStorage.setting; final _formKey = GlobalKey(); final TextEditingController urlController = TextEditingController(); final TextEditingController testUrlController = TextEditingController(); @override void initState() { super.initState(); urlController.text = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); testUrlController.text = setting.get(SettingBoxKey.proxyTestUrl, defaultValue: 'https://www.google.com'); } @override void dispose() { urlController.dispose(); testUrlController.dispose(); super.dispose(); } Future saveAndTest() async { if (!_formKey.currentState!.validate()) { return; } final url = urlController.text.trim(); if (url.isEmpty) { KazumiDialog.showToast(message: '请输入代理地址'); return; } final testUrl = testUrlController.text.trim().isEmpty ? 'https://www.google.com' : testUrlController.text.trim(); await setting.put(SettingBoxKey.proxyUrl, url); await setting.put(SettingBoxKey.proxyTestUrl, testUrl); // 重置配置状态,等待测试结果 await setting.put(SettingBoxKey.proxyConfigured, false); // 临时启用代理进行测试 await setting.put(SettingBoxKey.proxyEnable, true); ProxyManager.applyProxy(); try { await Request().get( testUrl, options: Options( sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), validateStatus: (status) => true, ), shouldRethrow: true, ).timeout(const Duration(seconds: 15)); await setting.put(SettingBoxKey.proxyConfigured, true); KazumiDialog.showToast(message: '测试成功'); } catch (e) { await setting.put(SettingBoxKey.proxyEnable, false); ProxyManager.clearProxy(); KazumiDialog.showToast(message: '代理连接失败'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar(title: Text('代理配置')), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( child: SizedBox( width: (MediaQuery.of(context).size.width > 800) ? 800 : null, child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: urlController, decoration: const InputDecoration( labelText: '代理地址', hintText: 'http://127.0.0.1:7890', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return '请输入代理地址'; } if (!ProxyUtils.isValidProxyUrl(value)) { return '格式错误,请使用 http://host:port 格式'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: testUrlController, decoration: const InputDecoration( labelText: '测试地址', hintText: 'https://www.google.com', border: OutlineInputBorder(), ), ), ], ), ), ), ), ), floatingActionButton: FloatingActionButton.extended( onPressed: saveAndTest, icon: const Icon(Icons.save), label: const Text('保存并测试'), ), ); } } ================================================ FILE: lib/pages/settings/proxy/proxy_module.dart ================================================ import 'package:kazumi/pages/settings/proxy/proxy_settings_page.dart'; import 'package:kazumi/pages/settings/proxy/proxy_editor_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class ProxyModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/", child: (_) => const ProxySettingsPage()); r.child("/editor", child: (_) => const ProxyEditorPage()); } } ================================================ FILE: lib/pages/settings/proxy/proxy_settings_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:hive_ce/hive.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_manager.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class ProxySettingsPage extends StatefulWidget { const ProxySettingsPage({super.key}); @override State createState() => _ProxySettingsPageState(); } class _ProxySettingsPageState extends State { Box setting = GStorage.setting; late bool proxyEnable; @override void initState() { super.initState(); proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } Future updateProxyEnable(bool value) async { if (value) { final proxyConfigured = setting.get(SettingBoxKey.proxyConfigured, defaultValue: false); if (!proxyConfigured) { KazumiDialog.showToast(message: '请先在代理配置中完成测试'); return; } await setting.put(SettingBoxKey.proxyEnable, true); ProxyManager.applyProxy(); } else { await setting.put(SettingBoxKey.proxyEnable, false); ProxyManager.clearProxy(); } setState(() { proxyEnable = value; }); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('代理设置')), body: SettingsList( maxWidth: 800, sections: [ SettingsSection( title: Text('代理', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { await updateProxyEnable(value ?? !proxyEnable); }, title: Text('启用代理', style: TextStyle(fontFamily: fontFamily)), description: Text('启用后网络请求将通过代理服务器', style: TextStyle(fontFamily: fontFamily)), initialValue: proxyEnable, ), SettingsTile.navigation( onPressed: (_) async { await Modular.to.pushNamed('/settings/proxy/editor'); setState(() { proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); }); }, title: Text('代理配置', style: TextStyle(fontFamily: fontFamily)), description: Text('配置代理服务器地址和认证信息', style: TextStyle(fontFamily: fontFamily)), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/settings/renderer_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class RendererSettings extends StatefulWidget { const RendererSettings({super.key}); @override State createState() => _RendererSettingsState(); } class _RendererSettingsState extends State { late final Box setting = GStorage.setting; late final ValueNotifier renderer = ValueNotifier( setting.get(SettingBoxKey.androidVideoRenderer, defaultValue: 'auto'), ); @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( title: Text('视频渲染器'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('选择合适的渲染器以获得最佳播放体验', style: TextStyle(fontFamily: fontFamily)), tiles: androidVideoRenderersList.entries .map((e) => SettingsTile.radioTile( title: Text(e.key, style: TextStyle(fontFamily: fontFamily)), description: Text(e.value, style: TextStyle(fontFamily: fontFamily)), radioValue: e.key, groupValue: renderer.value, onChanged: (String? value) { if (value != null) { setting.put( SettingBoxKey.androidVideoRenderer, value); setState(() { renderer.value = value; }); } }, )) .toList(), ), ], ), ); } } ================================================ FILE: lib/pages/settings/settings_module.dart ================================================ import 'package:kazumi/pages/settings/danmaku/danmaku_module.dart'; import 'package:kazumi/pages/about/about_module.dart'; import 'package:kazumi/pages/plugin_editor/plugin_module.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/history/history_module.dart'; import 'package:kazumi/pages/settings/interface_settings.dart'; import 'package:kazumi/pages/settings/theme_settings_page.dart'; import 'package:kazumi/pages/settings/player_settings.dart'; import 'package:kazumi/pages/settings/displaymode_settings.dart'; import 'package:kazumi/pages/settings/decoder_settings.dart'; import 'package:kazumi/pages/settings/renderer_settings.dart'; import 'package:kazumi/pages/settings/super_resolution_settings.dart'; import 'package:kazumi/pages/settings/proxy/proxy_module.dart'; import 'package:kazumi/pages/webdav_editor/webdav_module.dart'; import 'package:kazumi/pages/settings/keyboard_settings.dart'; import 'package:kazumi/pages/settings/download_settings.dart'; import 'package:kazumi/pages/download/download_page_module.dart'; class SettingsModule extends Module { @override void routes(r) { r.child("/theme", child: (_) => const ThemeSettingsPage()); r.child( "/theme/display", child: (_) => const SetDisplayMode(), ); r.child("/keyboard", child: (_) => const KeyboardSettingsPage()); r.child("/player", child: (_) => const PlayerSettingsPage()); r.child("/player/decoder", child: (_) => const DecoderSettings()); r.child("/player/renderer", child: (_) => const RendererSettings()); r.child("/interface", child: (_) => const InterfaceSettingsPage()); r.module("/proxy", module: ProxyModule()); r.child("/player/super", child: (_) => const SuperResolutionSettings()); r.module("/webdav", module: WebDavModule()); r.module("/about", module: AboutModule()); r.module("/plugin", module: PluginModule()); r.module("/history", module: HistoryModule()); r.module("/danmaku", module: DanmakuModule()); r.module("/download", module: DownloadModule()); r.child("/download-settings", child: (_) => const DownloadSettingsPage()); } } ================================================ FILE: lib/pages/settings/super_resolution_settings.dart ================================================ import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class SuperResolutionSettings extends StatefulWidget { const SuperResolutionSettings({super.key}); @override State createState() => _SuperResolutionSettingsState(); } class _SuperResolutionSettingsState extends State { late final Box setting = GStorage.setting; late bool promptOnEnable; late final ValueNotifier superResolutionType = ValueNotifier( setting .get(SettingBoxKey.defaultSuperResolutionType, defaultValue: 1) .toString(), ); @override void initState() { super.initState(); promptOnEnable = setting.get(SettingBoxKey.superResolutionWarn, defaultValue: false); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( title: Text('超分辨率'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text( '超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换视频渲染器为 gpu', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.radioTile( title: Text("OFF", style: TextStyle(fontFamily: fontFamily)), description: Text("默认禁用超分辨率", style: TextStyle(fontFamily: fontFamily)), radioValue: "1", groupValue: superResolutionType.value, onChanged: (String? value) { if (value != null) { setting.put(SettingBoxKey.defaultSuperResolutionType, int.tryParse(value) ?? 1); setState(() { superResolutionType.value = value; }); } }, ), SettingsTile.radioTile( title: Text("Efficiency", style: TextStyle(fontFamily: fontFamily)), description: Text("默认启用基于Anime4K的超分辨率 (效率优先)", style: TextStyle(fontFamily: fontFamily)), radioValue: "2", groupValue: superResolutionType.value, onChanged: (String? value) { if (value != null) { setting.put(SettingBoxKey.defaultSuperResolutionType, int.tryParse(value) ?? 1); setState(() { superResolutionType.value = value; }); } }, ), SettingsTile.radioTile( title: Text("Quality", style: TextStyle(fontFamily: fontFamily)), description: Text("默认启用基于Anime4K的超分辨率 (质量优先)", style: TextStyle(fontFamily: fontFamily)), radioValue: "3", groupValue: superResolutionType.value, onChanged: (String? value) { if (value != null) { setting.put(SettingBoxKey.defaultSuperResolutionType, int.tryParse(value) ?? 1); setState(() { superResolutionType.value = value; }); } }, ) ]), SettingsSection( title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( title: Text('关闭提示', style: TextStyle(fontFamily: fontFamily)), description: Text('关闭每次启用超分辨率时的提示', style: TextStyle(fontFamily: fontFamily)), initialValue: promptOnEnable, onToggle: (value) async { promptOnEnable = value ?? !promptOnEnable; await setting.put( SettingBoxKey.superResolutionWarn, promptOnEnable); if (mounted) setState(() {}); }, ), ], ), ], ), ); } } ================================================ FILE: lib/pages/settings/theme_settings_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/card/palette_card.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/settings/theme_provider.dart'; import 'package:kazumi/pages/popular/popular_controller.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/bean/settings/color_type.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:provider/provider.dart'; class ThemeSettingsPage extends StatefulWidget { const ThemeSettingsPage({super.key}); @override State createState() => _ThemeSettingsPageState(); } class _ThemeSettingsPageState extends State { Box setting = GStorage.setting; late dynamic defaultDanmakuArea; late dynamic defaultThemeMode; late dynamic defaultThemeColor; late bool oledEnhance; late bool useDynamicColor; late bool showWindowButton; late bool useSystemFont; final PopularController popularController = Modular.get(); late final ThemeProvider themeProvider; final MenuController menuController = MenuController(); @override void initState() { super.initState(); defaultThemeMode = setting.get(SettingBoxKey.themeMode, defaultValue: 'system'); defaultThemeColor = setting.get(SettingBoxKey.themeColor, defaultValue: 'default'); oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false); useDynamicColor = setting.get(SettingBoxKey.useDynamicColor, defaultValue: false); showWindowButton = setting.get(SettingBoxKey.showWindowButton, defaultValue: false); useSystemFont = setting.get(SettingBoxKey.useSystemFont, defaultValue: false); themeProvider = Provider.of(context, listen: false); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } void setTheme(Color? color) { var defaultDarkTheme = ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.dark, colorSchemeSeed: color, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024); var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme); themeProvider.setTheme( ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.light, colorSchemeSeed: color, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024), oledEnhance ? oledDarkTheme : defaultDarkTheme, ); defaultThemeColor = color?.value.toRadixString(16) ?? 'default'; setting.put(SettingBoxKey.themeColor, defaultThemeColor); } void resetTheme() { var defaultDarkTheme = ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.dark, colorSchemeSeed: Colors.green, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024); var oledDarkTheme = Utils.oledDarkTheme(defaultDarkTheme); themeProvider.setTheme( ThemeData( useMaterial3: true, fontFamily: themeProvider.currentFontFamily, brightness: Brightness.light, colorSchemeSeed: Colors.green, progressIndicatorTheme: progressIndicatorTheme2024, sliderTheme: sliderTheme2024, pageTransitionsTheme: pageTransitionsTheme2024), oledEnhance ? oledDarkTheme : defaultDarkTheme, ); defaultThemeColor = 'default'; setting.put(SettingBoxKey.themeColor, 'default'); } void updateTheme(String theme) async { if (theme == 'dark') { themeProvider.setThemeMode(ThemeMode.dark); } if (theme == 'light') { themeProvider.setThemeMode(ThemeMode.light); } if (theme == 'system') { themeProvider.setThemeMode(ThemeMode.system); } await setting.put(SettingBoxKey.themeMode, theme); setState(() { defaultThemeMode = theme; }); } void updateOledEnhance() { dynamic color; oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false); if (defaultThemeColor == 'default') { color = Colors.green; } else { color = Color(int.parse(defaultThemeColor, radix: 16)); } setTheme(color); } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('外观设置')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('外观', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { if (menuController.isOpen) { menuController.close(); } else { menuController.open(); } }, title: Text('深色模式', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: menuController, builder: (_, __, ___) { return Text( defaultThemeMode == 'light' ? '浅色' : (defaultThemeMode == 'dark' ? '深色' : '跟随系统'), style: TextStyle(fontFamily: fontFamily), ); }, menuChildren: [ MenuItemButton( requestFocusOnHover: false, onPressed: () => updateTheme('system'), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon( Icons.brightness_auto_rounded, color: defaultThemeMode == 'system' ? Theme.of(context).colorScheme.primary : null, ), SizedBox(width: 8), Text( '跟随系统', style: TextStyle( color: defaultThemeMode == 'system' ? Theme.of(context).colorScheme.primary : null, fontFamily: fontFamily, ), ), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () => updateTheme('light'), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon( Icons.light_mode_rounded, color: defaultThemeMode == 'light' ? Theme.of(context).colorScheme.primary : null, ), SizedBox(width: 8), Text( '浅色', style: TextStyle( color: defaultThemeMode == 'light' ? Theme.of(context) .colorScheme .primary : null, fontFamily: fontFamily), ), ], ), ), ), ), MenuItemButton( requestFocusOnHover: false, onPressed: () => updateTheme('dark'), child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Row( children: [ Icon( Icons.dark_mode_rounded, color: defaultThemeMode == 'dark' ? Theme.of(context).colorScheme.primary : null, ), SizedBox(width: 8), Text( '深色', style: TextStyle( color: defaultThemeMode == 'dark' ? Theme.of(context).colorScheme.primary : null, fontFamily: fontFamily, ), ), ], ), ), ), ), ], ), ), SettingsTile.navigation( enabled: !useDynamicColor, onPressed: (_) async { KazumiDialog.show(builder: (context) { return AlertDialog( title: Text('配色方案', style: TextStyle(fontFamily: fontFamily)), content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { final List> colorThemes = colorThemeTypes; return Wrap( alignment: WrapAlignment.center, spacing: 8, runSpacing: Utils.isDesktop() ? 8 : 0, children: [ ...colorThemes.map( (e) { final index = colorThemes.indexOf(e); return GestureDetector( onTap: () { index == 0 ? resetTheme() : setTheme(e['color']); KazumiDialog.dismiss(); }, child: Column( children: [ PaletteCard( color: e['color'], selected: (e['color'] .value .toRadixString(16) == defaultThemeColor || (defaultThemeColor == 'default' && index == 0)), ), Text(e['label']), ], ), ); }, ) ], ); }), ); }); }, title: Text('配色方案', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.switchTile( enabled: !Platform.isIOS, onToggle: (value) async { useDynamicColor = value ?? !useDynamicColor; await setting.put( SettingBoxKey.useDynamicColor, useDynamicColor); themeProvider.setDynamic(useDynamicColor); setState(() {}); }, title: Text('动态配色', style: TextStyle(fontFamily: fontFamily)), initialValue: useDynamicColor, ), SettingsTile.switchTile( onToggle: (value) async { useSystemFont = value ?? !useSystemFont; await setting.put( SettingBoxKey.useSystemFont, useSystemFont); themeProvider.setFontFamily(useSystemFont); dynamic color; if (defaultThemeColor == 'default') { color = Colors.green; } else { color = Color(int.parse(defaultThemeColor, radix: 16)); } setTheme(color); setState(() {}); }, title: Text('使用系统字体', style: TextStyle(fontFamily: fontFamily)), description: Text('关闭后使用 MI Sans 字体', style: TextStyle(fontFamily: fontFamily)), initialValue: useSystemFont, ), ], bottomInfo: Text('动态配色仅支持安卓12及以上和桌面平台', style: TextStyle(fontFamily: fontFamily)), ), SettingsSection( tiles: [ SettingsTile.switchTile( onToggle: (value) async { oledEnhance = value ?? !oledEnhance; await setting.put(SettingBoxKey.oledEnhance, oledEnhance); updateOledEnhance(); setState(() {}); }, title: Text('OLED优化', style: TextStyle(fontFamily: fontFamily)), description: Text('深色模式下使用纯黑背景', style: TextStyle(fontFamily: fontFamily)), initialValue: oledEnhance, ), ], ), if (Utils.isDesktop()) SettingsSection( tiles: [ SettingsTile.switchTile( onToggle: (value) async { showWindowButton = value ?? !showWindowButton; await setting.put( SettingBoxKey.showWindowButton, showWindowButton); setState(() {}); }, title: Text('使用系统标题栏', style: TextStyle(fontFamily: fontFamily)), description: Text('重启应用生效', style: TextStyle(fontFamily: fontFamily)), initialValue: showWindowButton, ), ], ), if (Platform.isAndroid) SettingsSection( tiles: [ SettingsTile.navigation( onPressed: (_) async { Modular.to.pushNamed('/settings/theme/display'); }, title: Text('屏幕帧率', style: TextStyle(fontFamily: fontFamily)), ), ], ), ], ), ), ); } } ================================================ FILE: lib/pages/timeline/timeline_controller.dart ================================================ import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:kazumi/utils/anime_season.dart'; import 'package:kazumi/repositories/collect_repository.dart'; import 'package:kazumi/modules/collect/collect_type.dart'; import 'package:mobx/mobx.dart'; part 'timeline_controller.g.dart'; class TimelineController = _TimelineController with _$TimelineController; abstract class _TimelineController with Store { final _collectRepository = Modular.get(); @observable ObservableList> bangumiCalendar = ObservableList>(); @observable String seasonString = ''; @observable bool isLoading = false; @observable bool isTimeOut = false; @observable late bool notShowAbandonedBangumis = _collectRepository.getTimelineNotShowAbandonedBangumis(); @observable late bool notShowWatchedBangumis = _collectRepository.getTimelineNotShowWatchedBangumis(); int sortType = 1; late DateTime selectedDate; void init() { selectedDate = DateTime.now(); seasonString = AnimeSeason(selectedDate).toString(); getSchedules(); } Future getSchedules() async { isLoading = true; isTimeOut = false; bangumiCalendar.clear(); final resBangumiCalendar = await BangumiHTTP.getCalendar(); bangumiCalendar.clear(); bangumiCalendar.addAll(resBangumiCalendar); changeSortType(sortType); isLoading = false; isTimeOut = bangumiCalendar.isEmpty; } Future getSchedulesBySeason() async { // 4次获取,每次最多20部 isLoading = true; isTimeOut = false; bangumiCalendar.clear(); var time = 0; const maxTime = 4; const limit = 20; var resBangumiCalendar = List.generate(7, (_) => []); for (time = 0; time < maxTime; time++) { final offset = time * limit; var newList = await BangumiHTTP.getCalendarBySearch( AnimeSeason(selectedDate).toSeasonStartAndEnd(), limit, offset); for (int i = 0; i < resBangumiCalendar.length; ++i) { resBangumiCalendar[i].addAll(newList[i]); } bangumiCalendar.clear(); bangumiCalendar.addAll(resBangumiCalendar); } isLoading = false; if (bangumiCalendar.isEmpty) { isTimeOut = true; } else { isTimeOut = bangumiCalendar.every((innerList) => innerList.isEmpty); } if (!isTimeOut) { changeSortType(sortType); } } void tryEnterSeason(DateTime date) { selectedDate = date; seasonString = "加载中 ٩(◦`꒳´◦)۶"; } /// 排序方式 /// 1. default /// 2. score /// 3. heat void changeSortType(int type) { if (type < 1 || type > 3) { return; } sortType = type; var resBangumiCalendar = bangumiCalendar.toList(); for (var dayList in resBangumiCalendar) { switch (sortType) { case 1: dayList.sort((a, b) => a.id.compareTo(b.id)); break; case 2: dayList.sort((a, b) => (b.ratingScore).compareTo(a.ratingScore)); break; case 3: dayList.sort((a, b) => (b.votes).compareTo(a.votes)); break; default: } } bangumiCalendar.clear(); bangumiCalendar.addAll(resBangumiCalendar); } @action Future setNotShowAbandonedBangumis(bool value) async { notShowAbandonedBangumis = value; await _collectRepository.updateTimelineNotShowAbandonedBangumis(value); } @action Future setNotShowWatchedBangumis(bool value) async { notShowWatchedBangumis = value; await _collectRepository.updateTimelineNotShowWatchedBangumis(value); } Set loadAbandonedBangumiIds() { return _collectRepository.getBangumiIdsByType(CollectType.abandoned); } Set loadWatchedBangumiIds() { return _collectRepository.getBangumiIdsByType(CollectType.watched); } } ================================================ FILE: lib/pages/timeline/timeline_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'timeline_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$TimelineController on _TimelineController, Store { late final _$bangumiCalendarAtom = Atom(name: '_TimelineController.bangumiCalendar', context: context); @override ObservableList> get bangumiCalendar { _$bangumiCalendarAtom.reportRead(); return super.bangumiCalendar; } @override set bangumiCalendar(ObservableList> value) { _$bangumiCalendarAtom.reportWrite(value, super.bangumiCalendar, () { super.bangumiCalendar = value; }); } late final _$seasonStringAtom = Atom(name: '_TimelineController.seasonString', context: context); @override String get seasonString { _$seasonStringAtom.reportRead(); return super.seasonString; } @override set seasonString(String value) { _$seasonStringAtom.reportWrite(value, super.seasonString, () { super.seasonString = value; }); } late final _$isLoadingAtom = Atom(name: '_TimelineController.isLoading', context: context); @override bool get isLoading { _$isLoadingAtom.reportRead(); return super.isLoading; } @override set isLoading(bool value) { _$isLoadingAtom.reportWrite(value, super.isLoading, () { super.isLoading = value; }); } late final _$isTimeOutAtom = Atom(name: '_TimelineController.isTimeOut', context: context); @override bool get isTimeOut { _$isTimeOutAtom.reportRead(); return super.isTimeOut; } @override set isTimeOut(bool value) { _$isTimeOutAtom.reportWrite(value, super.isTimeOut, () { super.isTimeOut = value; }); } late final _$notShowAbandonedBangumisAtom = Atom( name: '_TimelineController.notShowAbandonedBangumis', context: context); @override bool get notShowAbandonedBangumis { _$notShowAbandonedBangumisAtom.reportRead(); return super.notShowAbandonedBangumis; } bool _notShowAbandonedBangumisIsInitialized = false; @override set notShowAbandonedBangumis(bool value) { _$notShowAbandonedBangumisAtom.reportWrite( value, _notShowAbandonedBangumisIsInitialized ? super.notShowAbandonedBangumis : null, () { super.notShowAbandonedBangumis = value; _notShowAbandonedBangumisIsInitialized = true; }); } late final _$notShowWatchedBangumisAtom = Atom( name: '_TimelineController.notShowWatchedBangumis', context: context); @override bool get notShowWatchedBangumis { _$notShowWatchedBangumisAtom.reportRead(); return super.notShowWatchedBangumis; } bool _notShowWatchedBangumisIsInitialized = false; @override set notShowWatchedBangumis(bool value) { _$notShowWatchedBangumisAtom.reportWrite( value, _notShowWatchedBangumisIsInitialized ? super.notShowWatchedBangumis : null, () { super.notShowWatchedBangumis = value; _notShowWatchedBangumisIsInitialized = true; }); } late final _$setNotShowAbandonedBangumisAsyncAction = AsyncAction( '_TimelineController.setNotShowAbandonedBangumis', context: context); @override Future setNotShowAbandonedBangumis(bool value) { return _$setNotShowAbandonedBangumisAsyncAction .run(() => super.setNotShowAbandonedBangumis(value)); } late final _$setNotShowWatchedBangumisAsyncAction = AsyncAction( '_TimelineController.setNotShowWatchedBangumis', context: context); @override Future setNotShowWatchedBangumis(bool value) { return _$setNotShowWatchedBangumisAsyncAction .run(() => super.setNotShowWatchedBangumis(value)); } @override String toString() { return ''' bangumiCalendar: ${bangumiCalendar}, seasonString: ${seasonString}, isLoading: ${isLoading}, isTimeOut: ${isTimeOut}, notShowAbandonedBangumis: ${notShowAbandonedBangumis}, notShowWatchedBangumis: ${notShowWatchedBangumis} '''; } } ================================================ FILE: lib/pages/timeline/timeline_module.dart ================================================ import 'package:kazumi/pages/timeline/timeline_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; class TimelineModule extends Module { @override void routes(r) { r.child("/", child: (_) => const TimelinePage()); } } ================================================ FILE: lib/pages/timeline/timeline_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/menu/menu.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/pages/timeline/timeline_controller.dart'; import 'package:kazumi/bean/card/bangumi_timeline_card.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:provider/provider.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/anime_season.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; class TimelinePage extends StatefulWidget { const TimelinePage({super.key}); @override State createState() => _TimelinePageState(); } class _TimelinePageState extends State with SingleTickerProviderStateMixin { final TimelineController timelineController = Modular.get(); late NavigationBarState navigationBarState; TabController? tabController; late bool showRating; final List optionTabs = [ Tab(text: "排序方式"), Tab(text: "过滤器"), ]; @override void initState() { super.initState(); int weekday = DateTime.now().weekday - 1; tabController = TabController(vsync: this, length: tabs.length, initialIndex: weekday); navigationBarState = Provider.of(context, listen: false); showRating = GStorage.setting.get(SettingBoxKey.showRating, defaultValue: true); if (timelineController.bangumiCalendar.isEmpty) { timelineController.init(); } } @override void dispose() { tabController?.dispose(); super.dispose(); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } navigationBarState.updateSelectedIndex(0); Modular.to.navigate('/tab/popular/'); } DateTime generateDateTime(int year, String season) { switch (season) { case '冬': return DateTime(year, 1, 1); case '春': return DateTime(year, 4, 1); case '夏': return DateTime(year, 7, 1); case '秋': return DateTime(year, 10, 1); default: return DateTime.now(); } } final List tabs = const [ Tab(text: '一'), Tab(text: '二'), Tab(text: '三'), Tab(text: '四'), Tab(text: '五'), Tab(text: '六'), Tab(text: '日'), ]; final seasons = ['秋', '夏', '春', '冬']; String getStringByDateTime(DateTime d) { return d.year.toString() + Utils.getSeasonStringByMonth(d.month); } void showSeasonBottomSheet(BuildContext context) { final currDate = DateTime.now(); final years = List.generate(20, (index) => currDate.year - index); // 按年份分组生成可用季节 Map> yearSeasons = {}; for (final year in years) { List availableSeasons = []; for (final season in seasons) { final date = generateDateTime(year, season); if (currDate.isAfter(date)) { availableSeasons.add(date); } } if (availableSeasons.isNotEmpty) { yearSeasons[year] = availableSeasons; } } KazumiDialog.showBottomSheet( // context: context, backgroundColor: Theme.of(context).colorScheme.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), isScrollControlled: true, builder: (BuildContext context) { return DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.3, maxChildSize: 0.9, expand: false, builder: (context, scrollController) { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), child: Row( children: [ Icon( Icons.schedule, color: Theme.of(context).colorScheme.primary, size: 24, ), const SizedBox(width: 12), Text( '时间机器', style: Theme.of(context) .textTheme .titleLarge ?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), ), ], ), ), Divider( height: 1, color: Theme.of(context) .colorScheme .outlineVariant .withValues(alpha: 0.5), ), // 年份季节列表 Expanded( child: ListView.builder( controller: scrollController, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16), itemCount: yearSeasons.keys.length, itemBuilder: (context, index) { final year = yearSeasons.keys.elementAt(index); final availableSeasons = yearSeasons[year]!; return Padding( padding: const EdgeInsets.only(bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 年份标题 Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( children: [ Container( width: 4, height: 20, decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primary, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 12), Text( '$year年', style: Theme.of(context) .textTheme .titleMedium ?.copyWith( color: Theme.of(context) .colorScheme .onSurface, ), ), ], ), ), // 季节选择器 buildSeasonSegmentedButton( context, availableSeasons), ], ), ); }, ), ), ], ), ); }, ); }, ); } Widget buildSeasonSegmentedButton( BuildContext context, List availableSeasons) { DateTime? selectedSeason; for (final season in availableSeasons) { if (Utils.isSameSeason(timelineController.selectedDate, season)) { selectedSeason = season; break; } } final segments = availableSeasons.map((date) { final seasonName = Utils.getSeasonStringByMonth(date.month); return ButtonSegment( value: date, label: Text( seasonName, style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, ), ), icon: getSeasonIcon(seasonName), ); }).toList(); return SizedBox( width: double.infinity, child: SegmentedButton( segments: segments, selected: selectedSeason != null ? {selectedSeason} : {}, onSelectionChanged: (Set newSelection) { if (newSelection.isNotEmpty) { Navigator.pop(context); onSeasonSelected(newSelection.first); } }, multiSelectionEnabled: false, showSelectedIcon: false, emptySelectionAllowed: true, style: SegmentedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, selectedForegroundColor: Theme.of(context).colorScheme.onSecondaryContainer, selectedBackgroundColor: Theme.of(context).colorScheme.secondaryContainer, side: BorderSide( color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), width: 1, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ), ); } Widget getSeasonIcon(String seasonName) { IconData iconData; switch (seasonName) { case '春': iconData = Icons.eco; break; case '夏': iconData = Icons.wb_sunny; break; case '秋': iconData = Icons.park; break; case '冬': iconData = Icons.ac_unit; break; default: iconData = Icons.schedule; } return Icon( iconData, size: 18, ); } void onSeasonSelected(DateTime date) async { final currDate = DateTime.now(); timelineController.tryEnterSeason(date); if (Utils.isSameSeason(timelineController.selectedDate, currDate)) { await timelineController.getSchedules(); } else { await timelineController.getSchedulesBySeason(); } timelineController.seasonString = AnimeSeason(timelineController.selectedDate).toString(); } Widget showFilterSwitcher() { return Wrap( children: [ Observer( builder: (context) => InkWell( onTap: () { timelineController.setNotShowAbandonedBangumis( !timelineController.notShowAbandonedBangumis); }, child: ListTile( title: const Text('不显示已抛弃的番剧'), trailing: Switch( value: timelineController.notShowAbandonedBangumis, onChanged: (value) { timelineController.setNotShowAbandonedBangumis(value); }, ), ), ), ), Observer( builder: (context) => InkWell( onTap: () { timelineController.setNotShowWatchedBangumis( !timelineController.notShowWatchedBangumis); }, child: ListTile( title: const Text('不显示已看过的番剧'), trailing: Switch( value: timelineController.notShowWatchedBangumis, onChanged: (value) { timelineController.setNotShowWatchedBangumis(value); }, ), ), ), ), ], ); } Widget showSortSwitcher() { return Wrap( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('按热度排序'), onTap: () { KazumiDialog.dismiss(); timelineController.changeSortType(3); }, ), ListTile( title: const Text('按评分排序'), onTap: () { KazumiDialog.dismiss(); timelineController.changeSortType(2); }, ), ListTile( title: const Text('按时间排序'), onTap: () { KazumiDialog.dismiss(); timelineController.changeSortType(1); }, ), ], ), ], ); } Widget showTimelineOptionTabBar({required List options}) { return DefaultTabController( length: optionTabs.length, child: Scaffold( body: Column( children: [ PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Material( child: TabBar( tabs: optionTabs, ), ), ), Expanded( child: TabBarView( children: options, )) ], ))); } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: Scaffold( appBar: SysAppBar( needTopOffset: false, toolbarHeight: 104, bottom: TabBar( controller: tabController, tabs: tabs, indicatorColor: Theme.of(context).colorScheme.primary, ), title: InkWell( borderRadius: BorderRadius.circular(8), child: Observer(builder: (context) { return Text(timelineController.seasonString); }), onTap: () { showSeasonBottomSheet(context); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () async { KazumiDialog.showBottomSheet( backgroundColor: Theme.of(context).colorScheme.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), isScrollControlled: true, constraints: BoxConstraints( maxHeight: (MediaQuery.sizeOf(context).height >= LayoutBreakpoint.compact['height']!) ? MediaQuery.of(context).size.height * 1 / 4 : MediaQuery.of(context).size.height, maxWidth: (MediaQuery.sizeOf(context).width >= LayoutBreakpoint.medium['width']!) ? MediaQuery.of(context).size.width * 9 / 16 : MediaQuery.of(context).size.width, ), clipBehavior: Clip.antiAlias, context: context, builder: (context) { return showTimelineOptionTabBar( options: [showSortSwitcher(), showFilterSwitcher()]); }, ); }, child: const Icon(Icons.tune), ), body: Observer(builder: (context) { if (timelineController.isLoading && timelineController.bangumiCalendar.isEmpty) { return const Center( child: CircularProgressIndicator(), ); } if (timelineController.isTimeOut) { return Center( child: SizedBox( height: 400, child: GeneralErrorWidget(errMsg: '什么都没有找到 (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { onSeasonSelected(timelineController.selectedDate); }, text: '点击重试', ), ]), ), ); } return TabBarView( controller: tabController, children: contentGrid(timelineController.bangumiCalendar), ); }), ), ); } List contentGrid(List> bangumiCalendar) { List gridViewList = []; int crossCount = 1; if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) { crossCount = 2; } if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) { crossCount = 3; } double cardHeight = Utils.isDesktop() ? 160 : (Utils.isTablet() ? 140 : 120); for (var bangumiList in bangumiCalendar) { // 根据过滤器设置过滤番剧 var filteredList = bangumiList; if (timelineController.notShowAbandonedBangumis) { final abandonedBangumiIds = timelineController.loadAbandonedBangumiIds(); filteredList = filteredList .where((item) => !abandonedBangumiIds.contains(item.id)) .toList(); } if (timelineController.notShowWatchedBangumis) { final watchedBangumiIds = timelineController.loadWatchedBangumiIds(); filteredList = filteredList .where((item) => !watchedBangumiIds.contains(item.id)) .toList(); } gridViewList.add( CustomScrollView( slivers: [ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: StyleString.cardSpace - 2, crossAxisSpacing: StyleString.cardSpace, crossAxisCount: crossCount, mainAxisExtent: cardHeight + 12, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (filteredList.isEmpty) return null; final item = filteredList[index]; return BangumiTimelineCard( bangumiItem: item, cardHeight: cardHeight, showRating: showRating); }, childCount: filteredList.isNotEmpty ? filteredList.length : 10, ), ), ], ), ); } return gridViewList; } } ================================================ FILE: lib/pages/video/video_controller.dart ================================================ import 'dart:async'; import 'package:kazumi/modules/roads/road_module.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/pages/player/player_controller.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/repositories/download_repository.dart'; import 'package:kazumi/utils/download_manager.dart'; import 'package:kazumi/providers/video/providers.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:mobx/mobx.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:window_manager/window_manager.dart'; import 'package:kazumi/modules/bangumi/episode_item.dart'; import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/request/bangumi.dart'; import 'package:dio/dio.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; part 'video_controller.g.dart'; class VideoPageController = _VideoPageController with _$VideoPageController; abstract class _VideoPageController with Store { late BangumiItem bangumiItem; EpisodeInfo episodeInfo = EpisodeInfo.fromTemplate(); @observable var episodeCommentsList = ObservableList(); @observable bool loading = true; @observable String? errorMessage; @observable int currentEpisode = 1; @observable int currentRoad = 0; /// 全屏状态 @observable bool isFullscreen = false; /// 评论正序或倒序 @observable bool isCommentsAscending = false; /// 画中画状态 @observable bool isPip = false; /// 播放列表显示状态 @observable bool showTabBody = true; /// 上次观看位置 @observable int historyOffset = 0; /// 离线播放模式 @observable bool isOfflineMode = false; /// 离线视频本地路径 String? _offlineVideoPath; /// 和 bangumiItem 中的标题不同,此标题来自于视频源 String title = ''; String src = ''; @observable var roadList = ObservableList(); late Plugin currentPlugin; /// 离线模式下的虚拟插件名 String _offlinePluginName = ''; /// 用于取消正在进行的 queryRoads 操作 CancelToken? _queryRoadsCancelToken; final PluginsController pluginsController = Modular.get(); final HistoryController historyController = Modular.get(); final IDownloadRepository downloadRepository = Modular.get(); final IDownloadManager downloadManager = Modular.get(); final Box setting = GStorage.setting; /// 长生命周期的视频源提供者(页面生命周期内复用,WebView 实例在 Provider 内复用) WebViewVideoSourceProvider? _videoSourceProvider; /// 视频提供者日志流控制器 final StreamController _logStreamController = StreamController.broadcast(); Stream get logStream => _logStreamController.stream; StreamSubscription? _logSubscription; /// 初始化离线播放模式 void initForOfflinePlayback({ required BangumiItem bangumiItem, required String pluginName, required int episodeNumber, required String episodeName, required int road, required String videoPath, required List downloadedEpisodes, }) { this.bangumiItem = bangumiItem; _offlinePluginName = pluginName; currentRoad = road; title = bangumiItem.nameCn.isNotEmpty ? bangumiItem.nameCn : bangumiItem.name; isOfflineMode = true; _offlineVideoPath = videoPath; // 离线模式不需要解析视频源,直接设置 loading 为 false loading = false; // 构建仅包含已下载集数的 roadList _buildOfflineRoadList(downloadedEpisodes); // 离线模式下 roadList 长度为 1 , currentRoad 可能访问越界,需要校正 if (currentRoad < 0 || currentRoad >= roadList.length) { currentRoad = 0; } // currentEpisode 是列表中的 1-based 位置,而非实际集数编号 // 在 roadList.data 中查找 episodeNumber 对应的位置 final index = roadList[currentRoad].data.indexOf(episodeNumber.toString()); currentEpisode = index >= 0 ? index + 1 : 1; KazumiLogger().i( 'VideoPageController: initialized for offline playback, episode $episodeNumber (position: $currentEpisode)'); } /// 构建离线模式的 roadList void _buildOfflineRoadList(List episodes) { roadList.clear(); episodes.sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber)); // 使用 '播放列表1' 作为名称,与 UI 代码兼容 roadList.add(Road( name: '播放列表1', // data 存储实际的 episodeNumber(字符串形式),用于离线播放时查找本地文件 data: episodes.map((e) => e.episodeNumber.toString()).toList(), identifier: episodes .map((e) => e.episodeName.isNotEmpty ? e.episodeName : '第${e.episodeNumber}集') .toList(), )); } void resetOfflineMode() { isOfflineMode = false; _offlineVideoPath = null; _offlinePluginName = ''; } String? get offlineVideoPath => _offlineVideoPath; String get offlinePluginName => _offlinePluginName; /// 获取当前实际的集数编号 /// 在线模式下直接返回 currentEpisode /// 离线模式下从 roadList.data 中获取实际的 episodeNumber int get actualEpisodeNumber { if (isOfflineMode && roadList.isNotEmpty) { try { return int.parse(roadList[currentRoad].data[currentEpisode - 1]); } catch (_) { return currentEpisode; } } return currentEpisode; } Future changeEpisode(int episode, {int currentRoad = 0, int offset = 0}) async { currentEpisode = episode; this.currentRoad = currentRoad; errorMessage = null; if (isOfflineMode) { await _changeOfflineEpisode(episode, 0); return; } String chapterName = roadList[currentRoad].identifier[episode - 1]; KazumiLogger().i('VideoPageController: changed to $chapterName'); String urlItem = roadList[currentRoad].data[episode - 1]; if (urlItem.contains(currentPlugin.baseUrl) || urlItem.contains(currentPlugin.baseUrl.replaceAll('https', 'http'))) { urlItem = urlItem; } else { urlItem = currentPlugin.baseUrl + urlItem; } await _resolveWithProvider(urlItem, offset); } /// 离线模式下切换集数 /// [episode] 是列表中的位置(从 1 开始),需要从 roadList.data 中获取实际的 episodeNumber Future _changeOfflineEpisode(int episode, int offset) async { // 从 roadList.data 中获取实际的 episodeNumber final actualEpisodeNumber = int.tryParse(roadList[currentRoad].data[episode - 1]); if (actualEpisodeNumber == null) { KazumiLogger().e( 'VideoPageController: failed to parse episode number from roadList data: ${roadList[currentRoad].data[episode - 1]}'); KazumiDialog.showToast(message: '集数解析失败'); return; } final localPath = _getLocalVideoPath( bangumiItem.id, _offlinePluginName, actualEpisodeNumber, ); if (localPath == null) { KazumiDialog.showToast(message: '该集数未下载'); return; } _offlineVideoPath = localPath; loading = false; KazumiLogger().i( 'VideoPageController: offline episode changed to $actualEpisodeNumber (index: $episode), path: $localPath'); final params = PlaybackInitParams( videoUrl: localPath, offset: offset, isLocalPlayback: true, bangumiId: bangumiItem.id, pluginName: _offlinePluginName, episode: actualEpisodeNumber, httpHeaders: {}, adBlockerEnabled: false, episodeTitle: roadList[currentRoad].identifier[episode - 1], referer: '', currentRoad: currentRoad, ); final playerController = Modular.get(); await playerController.init(params); } /// 获取本地视频路径 String? _getLocalVideoPath( int bangumiId, String pluginName, int episodeNumber) { final episode = downloadRepository.getEpisode(bangumiId, pluginName, episodeNumber); return downloadManager.getLocalVideoPath(episode); } /// 使用 VideoSourceProvider 解析视频源 Future _resolveWithProvider(String url, int offset) async { _videoSourceProvider?.cancel(); loading = true; _videoSourceProvider ??= WebViewVideoSourceProvider(); await _logSubscription?.cancel(); _logSubscription = _videoSourceProvider!.onLog.listen((log) { if (!_logStreamController.isClosed) { _logStreamController.add(log); } }); try { final source = await _videoSourceProvider!.resolve( url, useLegacyParser: currentPlugin.useLegacyParser, offset: offset, ); loading = false; KazumiLogger() .i('VideoPageController: resolved video URL: ${source.url}'); final bool forceAdBlocker = setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false); final params = PlaybackInitParams( videoUrl: source.url, offset: source.offset, isLocalPlayback: false, bangumiId: bangumiItem.id, pluginName: currentPlugin.name, episode: currentEpisode, httpHeaders: { 'user-agent': currentPlugin.userAgent.isEmpty ? Utils.getRandomUA() : currentPlugin.userAgent, if (currentPlugin.referer.isNotEmpty) 'referer': currentPlugin.referer, }, adBlockerEnabled: forceAdBlocker || currentPlugin.adBlocker, episodeTitle: roadList[currentRoad].identifier[currentEpisode - 1], referer: currentPlugin.referer, currentRoad: currentRoad, ); final playerController = Modular.get(); await playerController.init(params); } on VideoSourceTimeoutException { loading = false; errorMessage = '视频解析超时,请重试'; } on VideoSourceCancelledException { KazumiLogger().i('VideoPageController: video URL resolution cancelled'); // 不设置 loading = false,因为可能是切换到新的集数 } catch (e) { loading = false; errorMessage = '视频解析失败:${e.toString()}'; } } /// 取消当前视频源解析并销毁 Provider(页面退出时调用) void cancelVideoSourceResolution() { _logSubscription?.cancel(); _logSubscription = null; if (!_logStreamController.isClosed) { _logStreamController.close(); } _videoSourceProvider?.dispose(); _videoSourceProvider = null; } Future queryBangumiEpisodeCommentsByID(int id, int episode) async { episodeCommentsList.clear(); episodeInfo = await BangumiHTTP.getBangumiEpisodeByID(id, episode); await BangumiHTTP.getBangumiCommentsByEpisodeID(episodeInfo.id) .then((value) { episodeCommentsList.addAll(value.commentList); }); if (!isCommentsAscending) { episodeCommentsList .sort((a, b) => b.comment.createdAt.compareTo(a.comment.createdAt)); } else { episodeCommentsList .sort((a, b) => a.comment.createdAt.compareTo(b.comment.createdAt)); } KazumiLogger().i( 'VideoPageController: loaded comments list length ${episodeCommentsList.length}'); } Future queryRoads(String url, String pluginName, {CancelToken? cancelToken}) async { if (cancelToken != null) { _queryRoadsCancelToken?.cancel(); _queryRoadsCancelToken = cancelToken; } else { _queryRoadsCancelToken?.cancel(); _queryRoadsCancelToken = CancelToken(); cancelToken = _queryRoadsCancelToken; } final PluginsController pluginsController = Modular.get(); roadList.clear(); for (Plugin plugin in pluginsController.pluginList) { if (plugin.name == pluginName) { roadList.addAll( await plugin.querychapterRoads(url, cancelToken: cancelToken)); } } KazumiLogger() .i('VideoPageController: road list length ${roadList.length}'); KazumiLogger().i( 'VideoPageController: first road episode count ${roadList[0].data.length}'); } void toggleSortOrder() { isCommentsAscending = !isCommentsAscending; episodeCommentsList.sort( (a, b) => isCommentsAscending ? a.comment.createdAt.compareTo(b.comment.createdAt) : b.comment.createdAt.compareTo(a.comment.createdAt), ); } void cancelQueryRoads() { if (_queryRoadsCancelToken != null) { if (!_queryRoadsCancelToken!.isCancelled) { _queryRoadsCancelToken!.cancel(); } } } void enterFullScreen() { isFullscreen = true; showTabBody = false; Utils.enterFullScreen(lockOrientation: false); } void exitFullScreen() { isFullscreen = false; Utils.exitFullScreen(); } void isDesktopFullscreen() async { if (Utils.isDesktop()) { isFullscreen = await windowManager.isFullScreen(); } } void handleOnEnterFullScreen() async { isFullscreen = true; showTabBody = false; } void handleOnExitFullScreen() async { isFullscreen = false; } } ================================================ FILE: lib/pages/video/video_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'video_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$VideoPageController on _VideoPageController, Store { late final _$episodeCommentsListAtom = Atom(name: '_VideoPageController.episodeCommentsList', context: context); @override ObservableList get episodeCommentsList { _$episodeCommentsListAtom.reportRead(); return super.episodeCommentsList; } @override set episodeCommentsList(ObservableList value) { _$episodeCommentsListAtom.reportWrite(value, super.episodeCommentsList, () { super.episodeCommentsList = value; }); } late final _$loadingAtom = Atom(name: '_VideoPageController.loading', context: context); @override bool get loading { _$loadingAtom.reportRead(); return super.loading; } @override set loading(bool value) { _$loadingAtom.reportWrite(value, super.loading, () { super.loading = value; }); } late final _$errorMessageAtom = Atom(name: '_VideoPageController.errorMessage', context: context); @override String? get errorMessage { _$errorMessageAtom.reportRead(); return super.errorMessage; } @override set errorMessage(String? value) { _$errorMessageAtom.reportWrite(value, super.errorMessage, () { super.errorMessage = value; }); } late final _$currentEpisodeAtom = Atom(name: '_VideoPageController.currentEpisode', context: context); @override int get currentEpisode { _$currentEpisodeAtom.reportRead(); return super.currentEpisode; } @override set currentEpisode(int value) { _$currentEpisodeAtom.reportWrite(value, super.currentEpisode, () { super.currentEpisode = value; }); } late final _$currentRoadAtom = Atom(name: '_VideoPageController.currentRoad', context: context); @override int get currentRoad { _$currentRoadAtom.reportRead(); return super.currentRoad; } @override set currentRoad(int value) { _$currentRoadAtom.reportWrite(value, super.currentRoad, () { super.currentRoad = value; }); } late final _$isFullscreenAtom = Atom(name: '_VideoPageController.isFullscreen', context: context); @override bool get isFullscreen { _$isFullscreenAtom.reportRead(); return super.isFullscreen; } @override set isFullscreen(bool value) { _$isFullscreenAtom.reportWrite(value, super.isFullscreen, () { super.isFullscreen = value; }); } late final _$isCommentsAscendingAtom = Atom(name: '_VideoPageController.isCommentsAscending', context: context); @override bool get isCommentsAscending { _$isCommentsAscendingAtom.reportRead(); return super.isCommentsAscending; } @override set isCommentsAscending(bool value) { _$isCommentsAscendingAtom.reportWrite(value, super.isCommentsAscending, () { super.isCommentsAscending = value; }); } late final _$isPipAtom = Atom(name: '_VideoPageController.isPip', context: context); @override bool get isPip { _$isPipAtom.reportRead(); return super.isPip; } @override set isPip(bool value) { _$isPipAtom.reportWrite(value, super.isPip, () { super.isPip = value; }); } late final _$showTabBodyAtom = Atom(name: '_VideoPageController.showTabBody', context: context); @override bool get showTabBody { _$showTabBodyAtom.reportRead(); return super.showTabBody; } @override set showTabBody(bool value) { _$showTabBodyAtom.reportWrite(value, super.showTabBody, () { super.showTabBody = value; }); } late final _$historyOffsetAtom = Atom(name: '_VideoPageController.historyOffset', context: context); @override int get historyOffset { _$historyOffsetAtom.reportRead(); return super.historyOffset; } @override set historyOffset(int value) { _$historyOffsetAtom.reportWrite(value, super.historyOffset, () { super.historyOffset = value; }); } late final _$isOfflineModeAtom = Atom(name: '_VideoPageController.isOfflineMode', context: context); @override bool get isOfflineMode { _$isOfflineModeAtom.reportRead(); return super.isOfflineMode; } @override set isOfflineMode(bool value) { _$isOfflineModeAtom.reportWrite(value, super.isOfflineMode, () { super.isOfflineMode = value; }); } late final _$roadListAtom = Atom(name: '_VideoPageController.roadList', context: context); @override ObservableList get roadList { _$roadListAtom.reportRead(); return super.roadList; } @override set roadList(ObservableList value) { _$roadListAtom.reportWrite(value, super.roadList, () { super.roadList = value; }); } @override String toString() { return ''' episodeCommentsList: ${episodeCommentsList}, loading: ${loading}, errorMessage: ${errorMessage}, currentEpisode: ${currentEpisode}, currentRoad: ${currentRoad}, isFullscreen: ${isFullscreen}, isCommentsAscending: ${isCommentsAscending}, isPip: ${isPip}, showTabBody: ${showTabBody}, historyOffset: ${historyOffset}, isOfflineMode: ${isOfflineMode}, roadList: ${roadList} '''; } } ================================================ FILE: lib/pages/video/video_module.dart ================================================ import 'package:kazumi/pages/video/video_page.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/player/player_controller.dart'; class VideoModule extends Module { @override void routes(r) { r.child("/", child: (_) => const VideoPage()); } @override void binds(i) { i.addSingleton(PlayerController.new); } } ================================================ FILE: lib/pages/video/video_page.dart ================================================ import 'dart:async'; import 'package:canvas_danmaku/models/danmaku_content_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/player/player_controller.dart'; import 'package:kazumi/pages/video/video_controller.dart'; import 'package:kazumi/pages/history/history_controller.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/pages/player/player_item.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/bean/appbar/drag_to_move_bar.dart' as dtb; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:kazumi/pages/player/episode_comments_sheet.dart'; import 'package:window_manager/window_manager.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/pages/download/download_controller.dart'; import 'package:kazumi/pages/download/download_episode_sheet.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/utils/timed_shutdown_service.dart'; class VideoPage extends StatefulWidget { const VideoPage({super.key}); @override State createState() => _VideoPageState(); } class _VideoPageState extends State with TickerProviderStateMixin, WindowListener { Box setting = GStorage.setting; final VideoPageController videoPageController = Modular.get(); final PlayerController playerController = Modular.get(); final HistoryController historyController = Modular.get(); final DownloadController downloadController = Modular.get(); late bool playResume; bool showDebugLog = false; List webviewLogLines = []; StreamSubscription? _logSubscription; final FocusNode keyboardFocus = FocusNode(); ScrollController scrollController = ScrollController(); late GridObserverController observerController; late AnimationController animation; late Animation _rightOffsetAnimation; late Animation _maskOpacityAnimation; late TabController tabController; // 当前播放列表 late int currentRoad; // disable animation. late final bool disableAnimations; // SyncPlayChatMessage late final StreamSubscription _syncChatSubscription; @override void initState() { super.initState(); windowManager.addListener(this); // Check fullscreen when enter video page // in case user use system controls to enter fullscreen outside video page videoPageController.isDesktopFullscreen(); tabController = TabController(length: 2, vsync: this); observerController = GridObserverController(controller: scrollController); animation = AnimationController( duration: const Duration(milliseconds: 120), vsync: this, ); _rightOffsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation( parent: animation, curve: Curves.easeOut, )); _maskOpacityAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeIn, )); playResume = setting.get(SettingBoxKey.playResume, defaultValue: true); disableAnimations = setting.get(SettingBoxKey.playerDisableAnimations, defaultValue: false); if (videoPageController.isOfflineMode) { // 离线模式:跳过 WebView 订阅,直接初始化播放器 _initOfflineMode(); } else { // 在线模式:设置 WebView 订阅 _initOnlineMode(); } _syncChatSubscription = playerController.syncPlayChatStream.listen((event) { final localUsername = playerController.syncplayController?.username ?? ''; final String displayText = '${event.username}:${event.message}'; // 只有在弹幕开启时渲染弹幕并确保是别人发送的弹幕 if (playerController.danmakuOn && event.username != localUsername && event.fromRemote) { playerController.danmakuController.addDanmaku( DanmakuContentItem( displayText, color: Colors.orange, isColorful: true, type: DanmakuItemType.bottom, extra: DateTime.now().millisecondsSinceEpoch, ), ); } }); } void _initOfflineMode() { videoPageController.showTabBody = true; videoPageController.historyOffset = 0; currentRoad = videoPageController.currentRoad; WidgetsBinding.instance.addPostFrameCallback((_) async { if (videoPageController.offlineVideoPath != null) { final params = PlaybackInitParams( videoUrl: videoPageController.offlineVideoPath!, offset: videoPageController.historyOffset, isLocalPlayback: true, bangumiId: videoPageController.bangumiItem.id, pluginName: videoPageController.offlinePluginName, episode: videoPageController.actualEpisodeNumber, httpHeaders: {}, adBlockerEnabled: false, episodeTitle: videoPageController .roadList[videoPageController.currentRoad] .identifier[videoPageController.currentEpisode - 1], referer: '', currentRoad: videoPageController.currentRoad, ); await playerController.init(params); } }); } void _initOnlineMode() { videoPageController.currentEpisode = 1; videoPageController.currentRoad = 0; videoPageController.historyOffset = 0; videoPageController.showTabBody = true; var progress = historyController.lastWatching( videoPageController.bangumiItem, videoPageController.currentPlugin.name); if (progress != null) { if (videoPageController.roadList.length > progress.road) { if (videoPageController.roadList[progress.road].data.length >= progress.episode) { videoPageController.currentEpisode = progress.episode; videoPageController.currentRoad = progress.road; if (playResume) { videoPageController.historyOffset = progress.progress.inSeconds; } } } } currentRoad = videoPageController.currentRoad; _logSubscription = videoPageController.logStream.listen((log) { if (mounted) { setState(() { webviewLogLines.add(log); if (webviewLogLines.length > 100) { webviewLogLines.removeAt(0); } }); } }); // 使用 Provider 模式启动播放 WidgetsBinding.instance.addPostFrameCallback((_) { changeEpisode(videoPageController.currentEpisode, currentRoad: videoPageController.currentRoad, offset: videoPageController.historyOffset); }); } @override void dispose() { try { windowManager.removeListener(this); } catch (_) {} try { observerController.controller?.dispose(); } catch (_) {} try { animation.dispose(); } catch (_) {} try { _syncChatSubscription.cancel(); } catch (_) {} try { _logSubscription?.cancel(); } catch (_) {} try { playerController.dispose(); } catch (e) { KazumiLogger().e( 'VideoPageController: failed to dispose playerController', error: e); } // 取消正在进行的视频源解析 videoPageController.cancelVideoSourceResolution(); if (!Utils.isDesktop()) { try { ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness(); } catch (_) {} } videoPageController.episodeInfo.reset(); videoPageController.episodeCommentsList.clear(); // 重置离线模式 videoPageController.resetOfflineMode(); Utils.unlockScreenRotation(); tabController.dispose(); // Cancel timed shutdown when leaving anime page TimedShutdownService().cancel(); super.dispose(); } // Handle fullscreen change invoked by system controls @override void onWindowEnterFullScreen() { videoPageController.handleOnEnterFullScreen(); } @override void onWindowLeaveFullScreen() { videoPageController.handleOnExitFullScreen(); } void showDebugConsole() { setState(() { showDebugLog = true; }); } void hideDebugConsole() { setState(() { showDebugLog = false; }); } void switchDebugConsole() { setState(() { showDebugLog = !showDebugLog; }); } void clearWebviewLog() { setState(() { webviewLogLines.clear(); }); } Future changeEpisode(int episode, {int currentRoad = 0, int offset = 0}) async { clearWebviewLog(); hideDebugConsole(); videoPageController.loading = true; videoPageController.errorMessage = null; videoPageController.episodeInfo.reset(); videoPageController.episodeCommentsList.clear(); await playerController.stop(); await videoPageController.changeEpisode(episode, currentRoad: currentRoad, offset: offset); } void menuJumpToCurrentEpisode() { Future.delayed(const Duration(milliseconds: 20), () async { await observerController.jumpTo( index: videoPageController.currentEpisode > 1 ? videoPageController.currentEpisode - 1 : videoPageController.currentEpisode); }); } void openTabBodyAnimated() { if (videoPageController.showTabBody) { if (!disableAnimations) { animation.forward(); } menuJumpToCurrentEpisode(); } } void closeTabBodyAnimated() { if (!disableAnimations) { animation.reverse(); Future.delayed(const Duration(milliseconds: 120), () { videoPageController.showTabBody = false; }); } else { videoPageController.showTabBody = false; } keyboardFocus.requestFocus(); } void onBackPressed(BuildContext context) async { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } if (videoPageController.isPip) { Utils.exitDesktopPIPWindow(); videoPageController.isPip = false; return; } if (videoPageController.isFullscreen && !Utils.isTablet()) { menuJumpToCurrentEpisode(); await Utils.exitFullScreen(); videoPageController.showTabBody = false; videoPageController.isFullscreen = false; return; } if (videoPageController.isFullscreen) { Utils.exitFullScreen(); videoPageController.isFullscreen = false; } Navigator.of(context).pop(); } /// Callback for timed shutdown - pauses video when timer expires void pauseForTimedShutdown() { if (playerController.playing) { playerController.pause(); } } /// 发送弹幕 由于接口限制, 暂时未提交云端 void sendDanmaku(String msg) async { keyboardFocus.requestFocus(); if (playerController.danDanmakus.isEmpty) { KazumiDialog.showToast( message: '当前剧集不支持弹幕发送的说', ); return; } if (msg.isEmpty) { KazumiDialog.showToast(message: '弹幕内容为空'); return; } else if (msg.length > 100) { KazumiDialog.showToast(message: '弹幕内容过长'); return; } final destination = playerController.danmakuDestination; if (destination == DanmakuDestination.chatRoom) { if (playerController.syncplayRoom.isEmpty) { KazumiDialog.showToast(message: '你还没有加入一起看,无法发送聊天室弹幕'); return; } final sender = playerController.syncplayController?.username ?? '我'; final String displayText = '$sender:$msg'; // 在播放器渲染自己发送的弹幕 playerController.danmakuController.addDanmaku( DanmakuContentItem( displayText, color: Colors.orange, isColorful: true, type: DanmakuItemType.bottom, extra: DateTime.now().millisecondsSinceEpoch, ), ); // 发送弹幕到聊天室 playerController.sendSyncPlayChatMessage(msg); } else { // Todo 接口方限制 playerController.danmakuController .addDanmaku(DanmakuContentItem(msg, selfSend: true)); } } void showMobileDanmakuInput() { final TextEditingController textController = TextEditingController(); showModalBottomSheet( shape: const BeveledRectangleBorder(), isScrollControlled: true, context: context, builder: (context) { return StatefulBuilder( builder: (context, setModalState) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, left: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Container( constraints: const BoxConstraints(maxHeight: 34), child: TextField( style: const TextStyle(fontSize: 15), controller: textController, autofocus: true, textAlignVertical: TextAlignVertical.center, decoration: const InputDecoration( filled: true, floatingLabelBehavior: FloatingLabelBehavior.never, hintText: '发个友善的弹幕见证当下', hintStyle: TextStyle(fontSize: 14), alignLabelWithHint: true, contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(20)), ), ), onSubmitted: (msg) { showDanmakuDestinationPickerAndSend(msg); textController.clear(); Navigator.pop(context); }, ), ), ), IconButton( onPressed: () { final msg = textController.text; Navigator.pop(context); showDanmakuDestinationPickerAndSend(msg); textController.clear(); }, icon: Icon( Icons.send_rounded, color: Theme.of(context).colorScheme.primary, ), ) ], ), ); }, ); }, ); } void showDanmakuDestinationPickerAndSend(String msg) async { if (msg.trim().isEmpty) { KazumiDialog.showToast(message: '弹幕内容为空'); return; } final DanmakuDestination? result = await showModalBottomSheet( context: context, shape: const BeveledRectangleBorder(), builder: (context) { return SafeArea( left: false, right: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('发送到聊天室'), onTap: () => Navigator.of(context).pop(DanmakuDestination.chatRoom), ), ListTile( title: const Text('发送到远程弹幕库'), onTap: () => Navigator.of(context).pop(DanmakuDestination.remoteDanmaku), ), const SizedBox(height: 8), ], ), ); }, ); if (result != null) { setState(() {}); playerController.danmakuDestination = result; sendDanmaku(msg); } } @override Widget build(BuildContext context) { final bool islandScape = MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height; WidgetsBinding.instance.addPostFrameCallback((_) { openTabBodyAnimated(); }); return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } onBackPressed(context); }, child: OrientationBuilder(builder: (context, orientation) { if (!Utils.isDesktop()) { if (orientation == Orientation.landscape && !videoPageController.isFullscreen) { videoPageController.enterFullScreen(); } else if (orientation == Orientation.portrait && videoPageController.isFullscreen) { videoPageController.exitFullScreen(); menuJumpToCurrentEpisode(); videoPageController.showTabBody = true; } } return Observer(builder: (context) { return Scaffold( appBar: null, body: SafeArea( top: !videoPageController.isFullscreen, // set iOS and Android navigation bar to immersive bottom: false, left: !videoPageController.isFullscreen, right: !videoPageController.isFullscreen, child: Stack( alignment: Alignment.centerRight, children: [ Column( children: [ Flexible( // make it unflexible when not wideScreen. flex: (islandScape) ? 1 : 0, child: Container( color: Colors.black, height: (islandScape) ? MediaQuery.sizeOf(context).height : MediaQuery.sizeOf(context).width * 9 / 16, width: MediaQuery.sizeOf(context).width, child: playerBody, ), ), // when not wideScreen, show tabBody on the bottom if (!islandScape) Expanded(child: tabBody), ], ), // when is wideScreen, show tabBody on the right side with SlideTransition or direct visibility if (islandScape && videoPageController.showTabBody) ...[ if (disableAnimations) ...[ sideTabMask, sideTabBody, ] else ...[ FadeTransition( opacity: _maskOpacityAnimation, child: sideTabMask, ), SlideTransition( position: _rightOffsetAnimation, child: sideTabBody, ), ], ], ], )), ); }); }), ); } Widget get sideTabBody { return SizedBox( height: MediaQuery.sizeOf(context).height, width: (!Utils.isDesktop() && !Utils.isTablet()) ? MediaQuery.sizeOf(context).height : (MediaQuery.sizeOf(context).width / 3 > 420 ? 420 : MediaQuery.sizeOf(context).width / 3), child: Container( color: Theme.of(context).canvasColor, child: GridViewObserver( controller: observerController, child: (Utils.isDesktop() || Utils.isTablet()) ? tabBody : Column( children: [ menuBar, menuBody, ], ), ), ), ); } Widget get sideTabMask { return GestureDetector( onTap: closeTabBodyAnimated, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ Colors.black.withValues(alpha: 0.5), Colors.transparent, ], ), ), width: double.infinity, height: double.infinity, ), ); } Widget get playerBody { return Stack( children: [ Positioned.fill( child: Stack( children: [ if (videoPageController.loading || playerController.loading || videoPageController.errorMessage != null) Container( color: Colors.black, child: Observer(builder: (context) { return Center( child: videoPageController.errorMessage != null ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error, size: 48), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric( horizontal: 32), child: Text( videoPageController.errorMessage!, style: const TextStyle( color: Colors.white, fontSize: 16), textAlign: TextAlign.center, ), ), ], ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( color: Theme.of(context) .colorScheme .tertiaryContainer), const SizedBox(height: 10), Text( videoPageController.loading ? '视频资源解析中' : '视频资源解析成功, 播放器加载中', style: const TextStyle(color: Colors.white), ), ], ), ); }), ), Visibility( visible: (videoPageController.loading || playerController.loading) && showDebugLog, child: Container( color: Colors.black, child: Align( alignment: Alignment.center, child: ListView.builder( shrinkWrap: true, itemCount: webviewLogLines.length, itemBuilder: (context, index) { return Text( webviewLogLines.isEmpty ? '' : webviewLogLines[index], style: const TextStyle( color: Colors.white, ), textAlign: TextAlign.center, ); }, ), ), ), ), Stack( children: [ Positioned( top: 0, left: 0, right: 0, child: EmbeddedNativeControlArea( requireOffset: !videoPageController.isFullscreen, child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => onBackPressed(context), ), const Expanded( child: dtb.DragToMoveArea( child: SizedBox(height: 40))), IconButton( icon: const Icon(Icons.refresh_outlined, color: Colors.white), onPressed: () { changeEpisode(videoPageController.currentEpisode, currentRoad: videoPageController.currentRoad); }, ), Visibility( visible: MediaQuery.sizeOf(context).width > MediaQuery.sizeOf(context).height, child: IconButton( onPressed: () { videoPageController.showTabBody = !videoPageController.showTabBody; openTabBodyAnimated(); }, icon: Icon( videoPageController.showTabBody ? Icons.menu_open : Icons.menu_open_outlined, color: Colors.white, ), ), ), IconButton( icon: Icon( showDebugLog ? Icons.bug_report : Icons.bug_report_outlined, color: Colors.white), onPressed: () { switchDebugConsole(); }, ), ], ), ), ), ], ), ], ), ), Positioned.fill( child: playerController.loading ? Container() : PlayerItem( openMenu: openTabBodyAnimated, locateEpisode: menuJumpToCurrentEpisode, changeEpisode: changeEpisode, onBackPressed: onBackPressed, keyboardFocus: keyboardFocus, sendDanmaku: sendDanmaku, disableAnimations: disableAnimations, showDanmakuDestinationPickerAndSend: showDanmakuDestinationPickerAndSend, pauseForTimedShutdown: pauseForTimedShutdown, ), ), ], ); } Widget get menuBar { return Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text(' 合集 '), Expanded( child: Text( videoPageController.title, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline, ), ), ), const SizedBox(width: 10), MenuAnchor( consumeOutsideTap: true, builder: (_, MenuController controller, __) { return SizedBox( height: 34, child: TextButton( style: ButtonStyle( padding: WidgetStateProperty.all(EdgeInsets.zero), ), onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, child: Text( '播放列表${currentRoad + 1} ', style: const TextStyle(fontSize: 13), ), ), ); }, menuChildren: List.generate( videoPageController.roadList.length, (int i) => MenuItemButton( onPressed: () { setState(() { currentRoad = i; }); }, child: Container( height: 48, constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, child: Text( '播放列表${i + 1}', style: TextStyle( color: i == currentRoad ? Theme.of(context).colorScheme.primary : null, ), ), ), ), ), ), ), ], ), ); } DownloadEpisode? _getEpisodeFromRecords( int episodeNumber, String episodePageUrl) { final bangumiId = videoPageController.bangumiItem.id; final pluginName = videoPageController.currentPlugin.name; for (final record in downloadController.records) { if (record.bangumiId == bangumiId && record.pluginName == pluginName) { if (episodePageUrl.isNotEmpty) { for (final episode in record.episodes.values) { if (episode.episodePageUrl == episodePageUrl) { return episode; } } } return record.episodes[episodeNumber]; } } return null; } Widget _buildDownloadStatusIcon(int episodeNumber, String episodePageUrl) { // 离线模式下不显示下载状态图标 if (videoPageController.isOfflineMode) return const SizedBox.shrink(); final episode = _getEpisodeFromRecords(episodeNumber, episodePageUrl); if (episode == null) return const SizedBox.shrink(); switch (episode.status) { case DownloadStatus.completed: return Icon(Icons.offline_pin, size: 16, color: Theme.of(context).colorScheme.primary); case DownloadStatus.downloading: return SizedBox( width: 16, height: 16, child: CircularProgressIndicator( value: episode.progressPercent, strokeWidth: 2, ), ); case DownloadStatus.failed: return Icon(Icons.error_outline, size: 16, color: Theme.of(context).colorScheme.error); case DownloadStatus.paused: return Icon(Icons.pause_circle_outline, size: 16, color: Theme.of(context).colorScheme.outline); case DownloadStatus.pending: case DownloadStatus.resolving: return SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ); default: return const SizedBox.shrink(); } } Widget get menuBody { return Observer( builder: (context) { var cardList = []; for (var road in videoPageController.roadList) { if (road.name == '播放列表${currentRoad + 1}') { int count = 1; for (var urlItem in road.data) { int count0 = count; cardList.add(Container( margin: const EdgeInsets.only(bottom: 4), child: Material( color: Theme.of(context).colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( onTap: () async { if (count0 == videoPageController.currentEpisode && videoPageController.currentRoad == currentRoad) { return; } KazumiLogger() .i('VideoPageController: video URL is $urlItem'); closeTabBodyAnimated(); changeEpisode(count0, currentRoad: currentRoad); }, child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (count0 == (videoPageController.currentEpisode) && currentRoad == videoPageController .currentRoad) ...[ Image.asset( 'assets/images/playing.gif', color: Theme.of(context).colorScheme.primary, height: 12, ), const SizedBox(width: 6) ], Expanded( child: Text( road.identifier[count0 - 1], maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: (count0 == videoPageController .currentEpisode && currentRoad == videoPageController.currentRoad) ? Theme.of(context).colorScheme.primary : Theme.of(context) .colorScheme .onSurface), )), _buildDownloadStatusIcon(count0, urlItem), const SizedBox(width: 2), ], ), const SizedBox(height: 3), ], ), ), ), ), )); count++; } } } return Expanded( child: Padding( padding: const EdgeInsets.only(top: 0, right: 8, left: 8), child: GridView.builder( scrollDirection: Axis.vertical, controller: scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 10, mainAxisSpacing: 5, mainAxisExtent: 70, ), itemCount: cardList.length, itemBuilder: (context, index) { return cardList[index]; }, ), ), ); }, ); } Widget get tabBody { int episodeNum = 0; episodeNum = Utils.extractEpisodeNumber(videoPageController .roadList[videoPageController.currentRoad] .identifier[videoPageController.currentEpisode - 1]); if (episodeNum == 0 || (!videoPageController.isOfflineMode && episodeNum > videoPageController .roadList[videoPageController.currentRoad] .identifier .length)) { episodeNum = videoPageController.isOfflineMode ? videoPageController.actualEpisodeNumber : videoPageController.currentEpisode; } return Container( color: Theme.of(context).canvasColor, child: DefaultTabController( length: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ TabBar( controller: tabController, dividerHeight: 0, isScrollable: true, tabAlignment: TabAlignment.start, labelPadding: const EdgeInsetsDirectional.only(start: 30, end: 30), onTap: (index) { if (index == 0) { menuJumpToCurrentEpisode(); } }, tabs: const [ Tab(text: '选集'), Tab(text: '评论'), ], ), if (MediaQuery.sizeOf(context).width <= MediaQuery.sizeOf(context).height) ...[ const Spacer(), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), border: Border.all( color: playerController.danmakuOn ? Theme.of(context).hintColor : Theme.of(context).disabledColor, width: 0.5, ), ), width: 120, height: 31, child: GestureDetector( onTap: () { if (playerController.danmakuOn && !videoPageController.loading) { showMobileDanmakuInput(); } else if (videoPageController.loading) { KazumiDialog.showToast(message: '请等待视频加载完成'); } else { KazumiDialog.showToast(message: '请先打开弹幕'); } }, child: Row( children: [ Text( playerController.danmakuOn ? ' 点我发弹幕 ' : ' 已关闭弹幕 ', softWrap: false, overflow: TextOverflow.clip, style: TextStyle( color: playerController.danmakuOn ? Theme.of(context).hintColor : Theme.of(context).disabledColor, ), ), Icon( Icons.send_rounded, size: 20, color: playerController.danmakuOn ? Theme.of(context).hintColor : Theme.of(context).disabledColor, ), ], ), ), ), ], const SizedBox(width: 8), ], ), Divider(height: Utils.isDesktop() ? 0.5 : 0.2), Expanded( child: TabBarView( controller: tabController, children: [ Stack( children: [ GridViewObserver( controller: observerController, child: Column( children: [ menuBar, menuBody, ], ), ), if (!videoPageController.isOfflineMode) Positioned( right: 16, bottom: 16, child: FloatingActionButton( child: const Icon(Icons.download_rounded), onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => DownloadEpisodeSheet(road: currentRoad), ); }, ), ), ], ), EpisodeInfo( episode: episodeNum, child: EpisodeCommentsSheet(), ), ], ), ), ], ), ), ); } } ================================================ FILE: lib/pages/webdav_editor/webdav_editor_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:kazumi/utils/webdav.dart'; class WebDavEditorPage extends StatefulWidget { const WebDavEditorPage({ super.key, }); @override State createState() => _WebDavEditorPageState(); } class _WebDavEditorPageState extends State { final TextEditingController webDavURLController = TextEditingController(); final TextEditingController webDavUsernameController = TextEditingController(); final TextEditingController webDavPasswordController = TextEditingController(); Box setting = GStorage.setting; bool passwordVisible = false; @override void initState() { super.initState(); webDavURLController.text = setting.get(SettingBoxKey.webDavURL, defaultValue: ''); webDavUsernameController.text = setting.get(SettingBoxKey.webDavUsername, defaultValue: ''); webDavPasswordController.text = setting.get(SettingBoxKey.webDavPassword, defaultValue: ''); } @override Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar( title: Text('WEBDAV编辑'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( child: SizedBox( width: (MediaQuery.of(context).size.width > 1000) ? 1000 : null, child: Column( children: [ TextField( controller: webDavURLController, decoration: const InputDecoration( labelText: 'URL', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: webDavUsernameController, decoration: const InputDecoration( labelText: 'Username', border: OutlineInputBorder()), ), const SizedBox(height: 20), TextField( controller: webDavPasswordController, obscureText: !passwordVisible, decoration: InputDecoration( labelText: 'Password', border: const OutlineInputBorder(), suffixIcon: IconButton( onPressed: () { setState(() { passwordVisible = !passwordVisible; }); }, icon: Icon(passwordVisible ? Icons.visibility_rounded : Icons.visibility_off_rounded), ), ), ), // const SizedBox(height: 20), // ExpansionTile( // title: const Text('高级选项'), // children: [], // ), ], ), ), ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.save), onPressed: () async { setting.put(SettingBoxKey.webDavURL, webDavURLController.text); setting.put( SettingBoxKey.webDavUsername, webDavUsernameController.text); setting.put( SettingBoxKey.webDavPassword, webDavPasswordController.text); var webDav = WebDav(); try { await webDav.init(); } catch (e) { KazumiDialog.showToast(message: '配置失败 ${e.toString()}'); await setting.put(SettingBoxKey.webDavEnable, false); return; } KazumiDialog.showToast(message: '配置成功, 开始测试'); try { await webDav.ping(); KazumiDialog.showToast(message: '测试成功'); } catch (e) { KazumiDialog.showToast(message: '测试失败 ${e.toString()}'); await setting.put(SettingBoxKey.webDavEnable, false); } }, ), ); } } ================================================ FILE: lib/pages/webdav_editor/webdav_module.dart ================================================ import 'package:kazumi/pages/webdav_editor/webdav_editor_page.dart'; import 'package:kazumi/pages/webdav_editor/webdav_setting.dart'; import 'package:flutter_modular/flutter_modular.dart'; class WebDavModule extends Module { @override void binds(i) {} @override void routes(r) { r.child("/", child: (_) => const WebDavSettingsPage()); r.child("/editor", child: (_) => const WebDavEditorPage(),); } } ================================================ FILE: lib/pages/webdav_editor/webdav_setting.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/webdav.dart'; import 'package:hive_ce/hive.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/appbar/sys_app_bar.dart'; import 'package:card_settings_ui/card_settings_ui.dart'; class WebDavSettingsPage extends StatefulWidget { const WebDavSettingsPage({super.key}); @override State createState() => _PlayerSettingsPageState(); } class _PlayerSettingsPageState extends State { Box setting = GStorage.setting; late bool webDavEnable; late bool webDavEnableHistory; late bool enableGitProxy; @override void initState() { super.initState(); webDavEnable = setting.get(SettingBoxKey.webDavEnable, defaultValue: false); webDavEnableHistory = setting.get(SettingBoxKey.webDavEnableHistory, defaultValue: false); enableGitProxy = setting.get(SettingBoxKey.enableGitProxy, defaultValue: false); } void onBackPressed(BuildContext context) { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); return; } } Future checkWebDav() async { var webDavURL = await setting.get(SettingBoxKey.webDavURL, defaultValue: ''); if (webDavURL == '') { await setting.put(SettingBoxKey.webDavEnable, false); KazumiDialog.showToast(message: '未找到有效的webdav配置'); return; } try { KazumiDialog.showToast(message: '尝试从WebDav同步'); var webDav = WebDav(); await webDav.downloadAndPatchHistory(); KazumiDialog.showToast(message: '同步成功'); } catch (e) { if (e.toString().contains('Error: Not Found')) { KazumiDialog.showToast(message: '配置成功, 这是一个不存在已有同步文件的全新WebDav'); } else { KazumiDialog.showToast(message: '同步失败 ${e.toString()}'); } } } Future updateWebdav() async { var webDavEnable = await setting.get(SettingBoxKey.webDavEnable, defaultValue: false); if (webDavEnable) { KazumiDialog.showToast(message: '尝试上传到WebDav'); var webDav = WebDav(); try { await webDav.ping(); try { await webDav.updateHistory(); KazumiDialog.showToast(message: '同步成功'); } catch (e) { KazumiDialog.showToast(message: '同步失败 ${e.toString()}'); } } catch (e) { KazumiDialog.showToast(message: 'WebDAV连接失败'); } } else { KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); } } Future downloadWebdav() async { var webDavEnable = await setting.get(SettingBoxKey.webDavEnable, defaultValue: false); if (webDavEnable) { KazumiDialog.showToast(message: '尝试从WebDav同步'); var webDav = WebDav(); try { await webDav.ping(); try { await webDav.downloadAndPatchHistory(); KazumiDialog.showToast(message: '同步成功'); } catch (e) { KazumiDialog.showToast(message: '同步失败 ${e.toString()}'); } } catch (e) { KazumiDialog.showToast(message: 'WebDAV连接失败'); } } else { KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); } } @override Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { onBackPressed(context); }, child: Scaffold( appBar: const SysAppBar(title: Text('同步设置')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( title: Text('Github', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { enableGitProxy = value ?? !enableGitProxy; await setting.put( SettingBoxKey.enableGitProxy, enableGitProxy); setState(() {}); }, title: Text('Github镜像', style: TextStyle(fontFamily: fontFamily)), description: Text('使用镜像访问规则托管仓库', style: TextStyle(fontFamily: fontFamily)), initialValue: enableGitProxy, ), ], ), SettingsSection( title: Text('WEBDAV', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { webDavEnable = value ?? !webDavEnable; if (!WebDav().initialized && webDavEnable) { try { await WebDav().init(); } catch (e) { webDavEnable = false; KazumiDialog.showToast(message: 'WEBDAV初始化失败 $e'); } } if (!webDavEnable) { webDavEnableHistory = false; await setting.put( SettingBoxKey.webDavEnableHistory, false); } await setting.put(SettingBoxKey.webDavEnable, webDavEnable); if (mounted) { setState(() {}); } }, title: Text('WEBDAV同步', style: TextStyle(fontFamily: fontFamily)), initialValue: webDavEnable, ), SettingsTile.switchTile( onToggle: (value) async { if (!webDavEnable) { KazumiDialog.showToast(message: '请先开启WEBDAV同步'); return; } webDavEnableHistory = value ?? !webDavEnableHistory; await setting.put( SettingBoxKey.webDavEnableHistory, webDavEnableHistory); setState(() {}); }, title: Text('观看记录同步', style: TextStyle(fontFamily: fontFamily)), description: Text('允许自动同步观看记录', style: TextStyle(fontFamily: fontFamily)), initialValue: webDavEnableHistory, ), SettingsTile.navigation( onPressed: (_) async { Modular.to.pushNamed('/settings/webdav/editor'); }, title: Text('WEBDAV配置', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( bottomInfo: Text('立即上传观看记录到WEBDAV', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( trailing: const Icon(Icons.cloud_upload_rounded), onPressed: (_) { updateWebdav(); }, title: Text('手动上传', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( bottomInfo: Text('立即下载观看记录到本地', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( trailing: const Icon(Icons.cloud_download_rounded), onPressed: (_) { downloadWebdav(); }, title: Text('手动下载', style: TextStyle(fontFamily: fontFamily)), ), ], ), ], ), ), ); } } ================================================ FILE: lib/plugins/anti_crawler_config.dart ================================================ /// 反反爬虫验证类型 /// /// - [imageCaptcha] (1): WebView 抓取验证码图片,引导用户手动输入后提交 /// - [autoClickButton] (2): WebView 检测到验证按钮后自动点击,无需用户交互 /// /// 保留整数表示以便将来新增第三种及更多验证方式时向后兼容。 class CaptchaType { static const int imageCaptcha = 1; static const int autoClickButton = 2; } /// 反反爬虫配置 /// /// 当网站对搜索请求返回验证码时,使用 WebView 加载搜索页, /// 根据 [captchaType] 采用不同策略完成验证,之后保存 Cookie 用于后续请求。 class AntiCrawlerConfig { /// 是否启用反反爬虫功能 bool enabled; /// 验证类型,见 [CaptchaType] 中的常量 /// /// - [CaptchaType.imageCaptcha] (1):图片验证码,需要用户手动输入 /// - [CaptchaType.autoClickButton] (2):自动点击验证按钮,无需用户交互 int captchaType; /// 验证码图片元素的 XPath 选择器(仅 captchaType == 1 时使用) /// 用于在 WebView 页面中定位验证码图片,通过 Canvas 抓取其像素 String captchaImage; /// 验证码输入框元素的 XPath 选择器(仅 captchaType == 1 时使用) /// 用于在 WebView 页面中定位供用户输入验证码的 input 元素 String captchaInput; /// 验证按钮元素的 XPath 选择器 /// /// - captchaType == 1:提交验证码的按钮,模拟点击提交 /// - captchaType == 2:目标验证按钮(如"我不是机器人"),检测到后自动点击 String captchaButton; AntiCrawlerConfig({ required this.enabled, required this.captchaType, required this.captchaImage, required this.captchaInput, required this.captchaButton, }); factory AntiCrawlerConfig.fromJson(Map json) { return AntiCrawlerConfig( enabled: json['enabled'] ?? false, captchaType: json['captchaType'] ?? CaptchaType.imageCaptcha, captchaImage: json['captchaImage'] ?? '', captchaInput: json['captchaInput'] ?? '', captchaButton: json['captchaButton'] ?? '', ); } factory AntiCrawlerConfig.empty() { return AntiCrawlerConfig( enabled: false, captchaType: CaptchaType.imageCaptcha, captchaImage: '', captchaInput: '', captchaButton: '', ); } Map toJson() { return { 'enabled': enabled, 'captchaType': captchaType, 'captchaImage': captchaImage, 'captchaInput': captchaInput, 'captchaButton': captchaButton, }; } AntiCrawlerConfig copyWith({ bool? enabled, int? captchaType, String? captchaImage, String? captchaInput, String? captchaButton, }) { return AntiCrawlerConfig( enabled: enabled ?? this.enabled, captchaType: captchaType ?? this.captchaType, captchaImage: captchaImage ?? this.captchaImage, captchaInput: captchaInput ?? this.captchaInput, captchaButton: captchaButton ?? this.captchaButton, ); } } ================================================ FILE: lib/plugins/plugin_cookie_manager.dart ================================================ import 'package:cookie_jar/cookie_jar.dart'; import 'package:kazumi/utils/logger.dart'; /// 每条规则的 Cookie 管理器 /// /// 为每条规则维护一个独立的内存 [CookieJar], /// 通过 [saveFromWebView] 将 WebView 捕获的 document.cookie 字符串 /// 解析后存入对应规则的 jar,用于后续 HTTP 请求的 CookieManager 拦截器。 /// Cookie 仅在当前 App 会话内有效,重启后需重新验证。 class PluginCookieManager { PluginCookieManager._(); static final PluginCookieManager instance = PluginCookieManager._(); final Map _jars = {}; CookieJar getJar(String pluginName) { return _jars.putIfAbsent(pluginName, () => CookieJar()); } Future saveFromWebView( String pluginName, String pageUrl, String cookieString) async { if (cookieString.trim().isEmpty) return; final uri = Uri.tryParse(pageUrl); if (uri == null) return; final jar = getJar(pluginName); final cookies = _parseCookieString(cookieString, uri); if (cookies.isEmpty) return; await jar.saveFromResponse(uri, cookies); KazumiLogger().i( '[PluginCookieManager] Saved ${cookies.length} cookies for $pluginName'); } /// 解析字符串为 [Cookie] 列表 List _parseCookieString(String raw, Uri uri) { final cookies = []; for (final part in raw.split(';')) { final trimmed = part.trim(); if (trimmed.isEmpty) continue; final eqIndex = trimmed.indexOf('='); if (eqIndex <= 0) continue; final name = trimmed.substring(0, eqIndex).trim(); final value = trimmed.substring(eqIndex + 1).trim(); try { final cookie = Cookie(name, value) ..domain = uri.host ..path = '/'; cookies.add(cookie); } catch (_) {} } return cookies; } void clearCookies(String pluginName) { _jars.remove(pluginName); } bool hasCookies(String pluginName) { return _jars.containsKey(pluginName); } } ================================================ FILE: lib/plugins/plugin_install_time_tracker.dart ================================================ // 记录规则安装时间 // 使用文件修改时间作为规则的安装时间 class PluginInstallTimeTracker { // 记录规则安装时间的映射 final Map _installTimes = {}; // 设置规则的安装时间 void setInstallTime(String pluginName, int timestamp) { _installTimes[pluginName] = timestamp; } // 获取规则的安装时间,如果不存在返回0 int getInstallTime(String pluginName) { return _installTimes[pluginName] ?? 0; } } ================================================ FILE: lib/plugins/plugin_validity_tracker.dart ================================================ // 记录规则有效性状态 // 目前仅追踪搜索有效性:在本次启动后,规则是否成功返回过搜索结果 class PluginValidityTracker { // 记录搜索有效的规则集合 final Set _searchValidPlugins = {}; // 标记规则搜索有效(成功返回过搜索结果) void markSearchValid(String pluginName) { _searchValidPlugins.add(pluginName); } // 检查规则搜索是否有效(是否成功返回过搜索结果) bool isSearchValid(String pluginName) { return _searchValidPlugins.contains(pluginName); } } ================================================ FILE: lib/plugins/plugins.dart ================================================ import 'package:dio/dio.dart'; import 'package:kazumi/modules/search/plugin_search_module.dart'; import 'package:kazumi/modules/roads/road_module.dart'; import 'package:kazumi/request/request.dart'; import 'package:html/parser.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/plugins/anti_crawler_config.dart'; import 'package:kazumi/plugins/plugin_cookie_manager.dart'; /// Thrown by [Plugin.queryBangumi] when the response contains a CAPTCHA challenge /// (i.e. the [AntiCrawlerConfig.captchaImage] XPath selector matches something /// in the returned HTML). class CaptchaRequiredException implements Exception { final String pluginName; const CaptchaRequiredException(this.pluginName); @override String toString() => 'CaptchaRequiredException: $pluginName requires captcha verification'; } /// Thrown by [Plugin.queryBangumi] when the search request succeeds but the /// XPath selectors return no results. class NoResultException implements Exception { final String pluginName; const NoResultException(this.pluginName); @override String toString() => 'NoResultException: $pluginName returned no search results'; } /// Thrown by [Plugin.queryBangumi] when the HTTP request or HTML parsing /// fails for reasons other than a captcha challenge. class SearchErrorException implements Exception { final String pluginName; final Object? cause; const SearchErrorException(this.pluginName, {this.cause}); @override String toString() => 'SearchErrorException: $pluginName search failed${cause != null ? ' ($cause)' : ''}'; } class Plugin { String api; String type; String name; String version; bool muliSources; bool useWebview; /// Deprecated (always true) bool useNativePlayer; bool usePost; bool useLegacyParser; bool adBlocker; String userAgent; String baseUrl; String searchURL; String searchList; String searchName; String searchResult; String chapterRoads; String chapterResult; String referer; AntiCrawlerConfig antiCrawlerConfig; Plugin({ required this.api, required this.type, required this.name, required this.version, required this.muliSources, required this.useWebview, required this.useNativePlayer, required this.usePost, required this.useLegacyParser, required this.adBlocker, required this.userAgent, required this.baseUrl, required this.searchURL, required this.searchList, required this.searchName, required this.searchResult, required this.chapterRoads, required this.chapterResult, required this.referer, AntiCrawlerConfig? antiCrawlerConfig, }) : antiCrawlerConfig = antiCrawlerConfig ?? AntiCrawlerConfig.empty(); factory Plugin.fromJson(Map json) { return Plugin( api: json['api'], type: json['type'], name: json['name'], version: json['version'], muliSources: json['muliSources'], useWebview: json['useWebview'], useNativePlayer: json['useNativePlayer'], usePost: json['usePost'] ?? false, useLegacyParser: json['useLegacyParser'] ?? false, adBlocker: json['adBlocker'] ?? false, userAgent: json['userAgent'], baseUrl: json['baseURL'], searchURL: json['searchURL'], searchList: json['searchList'], searchName: json['searchName'], searchResult: json['searchResult'], chapterRoads: json['chapterRoads'], chapterResult: json['chapterResult'], referer: json['referer'] ?? '', antiCrawlerConfig: json['antiCrawlerConfig'] != null ? AntiCrawlerConfig.fromJson( Map.from(json['antiCrawlerConfig'])) : AntiCrawlerConfig.empty()); } factory Plugin.fromTemplate() { return Plugin( api: Api.apiLevel.toString(), type: 'anime', name: '', version: '', muliSources: true, useWebview: true, useNativePlayer: true, usePost: false, useLegacyParser: false, adBlocker: false, userAgent: '', baseUrl: '', searchURL: '', searchList: '', searchName: '', searchResult: '', chapterRoads: '', chapterResult: '', referer: '', antiCrawlerConfig: AntiCrawlerConfig.empty()); } Map toJson() { final Map data = {}; data['api'] = api; data['type'] = type; data['name'] = name; data['version'] = version; data['muliSources'] = muliSources; data['useWebview'] = useWebview; data['useNativePlayer'] = useNativePlayer; data['usePost'] = usePost; data['useLegacyParser'] = useLegacyParser; data['adBlocker'] = adBlocker; data['userAgent'] = userAgent; data['baseURL'] = baseUrl; data['searchURL'] = searchURL; data['searchList'] = searchList; data['searchName'] = searchName; data['searchResult'] = searchResult; data['chapterRoads'] = chapterRoads; data['chapterResult'] = chapterResult; data['referer'] = referer; data['antiCrawlerConfig'] = antiCrawlerConfig.toJson(); return data; } Future queryBangumi(String keyword, {bool shouldRethrow = false}) async { try { String queryURL = searchURL.replaceAll('@keyword', keyword); dynamic resp; List searchItems = []; final String cookieHeader = await _cookieHeaderFor(queryURL); if (usePost) { Uri uri = Uri.parse(queryURL); Map queryParams = uri.queryParameters; Uri postUri = Uri( scheme: uri.scheme, host: uri.host, path: uri.path, ); var httpHeaders = { 'referer': '$baseUrl/', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Language': Utils.getRandomAcceptedLanguage(), 'Connection': 'keep-alive', if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader, }; resp = await Request().post(postUri.toString(), options: Options(headers: httpHeaders), extra: {'customError': ''}, data: queryParams, shouldRethrow: shouldRethrow); } else { var httpHeaders = { 'referer': '$baseUrl/', 'Accept-Language': Utils.getRandomAcceptedLanguage(), 'Connection': 'keep-alive', if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader, }; resp = await Request().get(queryURL, options: Options(headers: httpHeaders), shouldRethrow: shouldRethrow, extra: {'customError': ''}); } var htmlString = resp.data.toString(); var htmlElement = parse(htmlString).documentElement!; // Detect captcha challenge: if antiCrawlerConfig is enabled, check both // captchaImage and captchaButton XPaths — if either matches, throw so // callers can show the dedicated captcha UI instead of a generic error. if (antiCrawlerConfig.enabled) { final List detectionXpaths = [ antiCrawlerConfig.captchaImage, antiCrawlerConfig.captchaButton, ].where((x) => x.isNotEmpty).toList(); final bool captchaDetected = detectionXpaths.any( (xpath) => htmlElement.queryXPath(xpath).node != null); if (captchaDetected) { KazumiLogger().w('Plugin: $name detected captcha challenge in search response'); throw CaptchaRequiredException(name); } } htmlElement.queryXPath(searchList).nodes.forEach((element) { try { SearchItem searchItem = SearchItem( name: element.queryXPath(searchName).node!.text?.trim() ?? '', src: element.queryXPath(searchResult).node!.attributes['href'] ?? '', ); searchItems.add(searchItem); KazumiLogger().i( 'Plugin: $name ${element.queryXPath(searchName).node!.text ?? ''} $baseUrl${element.queryXPath(searchResult).node!.attributes['href'] ?? ''}'); } catch (_) {} }); if (searchItems.isEmpty) throw NoResultException(name); return PluginSearchResponse(pluginName: name, data: searchItems); } on CaptchaRequiredException { rethrow; } on NoResultException { rethrow; } catch (e, st) { KazumiLogger().w('Plugin: $name search failed', error: e, stackTrace: st); if (shouldRethrow) throw SearchErrorException(name, cause: e); return PluginSearchResponse(pluginName: name, data: []); } } Future> querychapterRoads(String url, {CancelToken? cancelToken}) async { List roadList = []; if (!url.contains('https')) { url = url.replaceAll('http', 'https'); } String queryURL = ''; if (url.contains(baseUrl)) { queryURL = url; } else { queryURL = baseUrl + url; } var httpHeaders = { 'referer': '$baseUrl/', 'Accept-Language': Utils.getRandomAcceptedLanguage(), 'Connection': 'keep-alive', }; try { var resp = await Request().get(queryURL, options: Options(headers: httpHeaders), cancelToken: cancelToken); var htmlString = resp.data.toString(); var htmlElement = parse(htmlString).documentElement!; int count = 1; htmlElement.queryXPath(chapterRoads).nodes.forEach((element) { try { List chapterUrlList = []; List chapterNameList = []; element.queryXPath(chapterResult).nodes.forEach((item) { String itemUrl = item.node.attributes['href'] ?? ''; String itemName = item.node.text ?? ''; chapterUrlList.add(itemUrl); chapterNameList.add(itemName.replaceAll(RegExp(r'\s+'), '')); }); if (chapterUrlList.isNotEmpty && chapterNameList.isNotEmpty) { Road road = Road( name: '播放列表$count', data: chapterUrlList, identifier: chapterNameList); roadList.add(road); count++; } } catch (_) {} }); } catch (_) {} return roadList; } Future testSearchRequest(String keyword, {bool shouldRethrow = false, CancelToken? cancelToken}) async { String queryURL = searchURL.replaceAll('@keyword', keyword); dynamic resp; if (usePost) { Uri uri = Uri.parse(queryURL); Map queryParams = uri.queryParameters; Uri postUri = Uri( scheme: uri.scheme, host: uri.host, path: uri.path, ); var httpHeaders = { 'referer': '$baseUrl/', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Language': Utils.getRandomAcceptedLanguage(), 'Connection': 'keep-alive', }; resp = await Request().post(postUri.toString(), options: Options(headers: httpHeaders), extra: {'customError': ''}, data: queryParams, shouldRethrow: shouldRethrow, cancelToken: cancelToken); } else { var httpHeaders = { 'referer': '$baseUrl/', 'Accept-Language': Utils.getRandomAcceptedLanguage(), 'Connection': 'keep-alive', }; resp = await Request().get(queryURL, options: Options(headers: httpHeaders), shouldRethrow: shouldRethrow, extra: {'customError': ''}, cancelToken: cancelToken); } return resp.data.toString(); } Future _cookieHeaderFor(String url) async { if (!PluginCookieManager.instance.hasCookies(name)) return ''; final uri = Uri.tryParse(url); if (uri == null) return ''; try { final cookies = await PluginCookieManager.instance.getJar(name).loadForRequest(uri); if (cookies.isEmpty) return ''; return cookies.map((c) => '${c.name}=${c.value}').join('; '); } catch (_) { return ''; } } String buildFullUrl(String urlItem) { if (urlItem.contains(baseUrl) || urlItem.contains(baseUrl.replaceAll('https', 'http'))) { return urlItem; } return baseUrl + urlItem; } Map buildHttpHeaders() { return { 'user-agent': userAgent.isEmpty ? Utils.getRandomUA() : userAgent, if (referer.isNotEmpty) 'referer': referer, }; } PluginSearchResponse testQueryBangumi(String htmlString) { List searchItems = []; var htmlElement = parse(htmlString).documentElement!; htmlElement.queryXPath(searchList).nodes.forEach((element) { try { SearchItem searchItem = SearchItem( name: element.queryXPath(searchName).node!.text?.trim() ?? '', src: element.queryXPath(searchResult).node!.attributes['href'] ?? '', ); searchItems.add(searchItem); KazumiLogger().i( 'Plugin: $name ${element.queryXPath(searchName).node!.text ?? ''} $baseUrl${element.queryXPath(searchResult).node!.attributes['href'] ?? ''}'); } catch (_) {} }); PluginSearchResponse pluginSearchResponse = PluginSearchResponse(pluginName: name, data: searchItems); return pluginSearchResponse; } } ================================================ FILE: lib/plugins/plugins_controller.dart ================================================ import 'dart:io'; import 'dart:convert'; import 'package:mobx/mobx.dart'; import 'package:flutter/services.dart' show rootBundle, AssetManifest; import 'package:path_provider/path_provider.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/plugins/plugin_validity_tracker.dart'; import 'package:kazumi/plugins/plugin_install_time_tracker.dart'; import 'package:kazumi/request/plugin.dart'; import 'package:kazumi/modules/plugin/plugin_http_module.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/request/api.dart'; part 'plugins_controller.g.dart'; // 从 1.5.1 版本开始,规则文件储存在单一的 plugins.json 文件中。 // 之前的版本中,规则以分离文件形式存储,版本更新后将这些分离文件合并为单一的 plugins.json 文件。 class PluginsController = _PluginsController with _$PluginsController; abstract class _PluginsController with Store { @observable ObservableList pluginList = ObservableList.of([]); @observable ObservableList pluginHTTPList = ObservableList.of([]); // 规则有效性追踪器 final validityTracker = PluginValidityTracker(); // 规则安装时间追踪器 final installTimeTracker = PluginInstallTimeTracker(); String pluginsFileName = "plugins.json"; Directory? oldPluginDirectory; Directory? newPluginDirectory; // Initializes the plugin directory and loads all plugins Future init() async { final directory = await getApplicationSupportDirectory(); oldPluginDirectory = Directory('${directory.path}/plugins'); if (!await oldPluginDirectory!.exists()) { await oldPluginDirectory!.create(recursive: true); } newPluginDirectory = Directory('${directory.path}/plugins/v2'); if (!await newPluginDirectory!.exists()) { await newPluginDirectory!.create(recursive: true); } await loadAllPlugins(); } // Loads all plugins from the directory, populates the plugin list, and saves to plugins.json if needed Future loadAllPlugins() async { pluginList.clear(); KazumiLogger() .i('Plugins Directory: ${newPluginDirectory!.path}'); if (await newPluginDirectory!.exists()) { final pluginsFile = File('${newPluginDirectory!.path}/$pluginsFileName'); if (await pluginsFile.exists()) { final jsonString = await pluginsFile.readAsString(); pluginList.addAll(getPluginListFromJson(jsonString)); KazumiLogger() .i('Plugin: Current Plugin number: ${pluginList.length}'); } else { // No plugins.json var jsonFiles = await getPluginFiles(); for (var filePath in jsonFiles) { final file = File(filePath); final jsonString = await file.readAsString(); final data = jsonDecode(jsonString); final plugin = Plugin.fromJson(data); pluginList.add(plugin); await file.delete(recursive: true); } savePlugins(); } } else { KazumiLogger().w('Plugin: plugin directory does not exist'); } } // Retrieves a list of JSON plugin file paths from the plugin directory Future> getPluginFiles() async { if (await oldPluginDirectory!.exists()) { final jsonFiles = oldPluginDirectory! .listSync() .where((file) => file.path.endsWith('.json') && file is File) .map((file) => file.path) .toList(); return jsonFiles; } else { return []; } } // Copies plugin JSON files from the assets to the plugin directory Future copyPluginsToExternalDirectory() async { final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle); final assets = assetManifest.listAssets(); final jsonFiles = assets.where((String asset) => asset.startsWith('assets/plugins/') && asset.endsWith('.json')); for (var filePath in jsonFiles) { final jsonString = await rootBundle.loadString(filePath); final plugin = Plugin.fromJson(jsonDecode(jsonString)); pluginList.add(plugin); } await savePlugins(); KazumiLogger().i( 'Plugin: ${jsonFiles.length} plugin files copied to ${newPluginDirectory!.path}'); } List pluginListToJson() { final List json = []; for (var plugin in pluginList) { json.add(plugin.toJson()); } return json; } // Converts a JSON string into a list of Plugin objects. List getPluginListFromJson(String jsonString) { List json = jsonDecode(jsonString); List plugins = []; for (var j in json) { plugins.add(Plugin.fromJson(j)); } return plugins; } Future removePlugin(Plugin plugin) async { pluginList.removeWhere((p) => p.name == plugin.name); await savePlugins(); } // Update or add plugin void updatePlugin(Plugin plugin) { bool flag = false; for (int i = 0; i < pluginList.length; ++i) { if (pluginList[i].name == plugin.name) { pluginList.replaceRange(i, i + 1, [plugin]); flag = true; break; } } if (!flag) { pluginList.add(plugin); } savePlugins(); } void onReorder(int oldIndex, int newIndex) { if (oldIndex < newIndex) { newIndex -= 1; } final plugin = pluginList.removeAt(oldIndex); pluginList.insert(newIndex, plugin); savePlugins(); } Future savePlugins() async { final jsonData = jsonEncode(pluginListToJson()); final pluginsFile = File('${newPluginDirectory!.path}/$pluginsFileName'); await pluginsFile.writeAsString(jsonData); KazumiLogger().i('Plugin: updated plugin file $pluginsFileName'); } Future queryPluginHTTPList() async { pluginHTTPList.clear(); var pluginHTTPListRes = await PluginHTTP.getPluginList(); pluginHTTPList.addAll(pluginHTTPListRes); } Future queryPluginHTTP(String name) async { Plugin? plugin; plugin = await PluginHTTP.getPlugin(name); return plugin; } String pluginStatus(PluginHTTPItem pluginHTTPItem) { String pluginStatus = 'install'; for (Plugin plugin in pluginList) { if (pluginHTTPItem.name == plugin.name) { if (pluginHTTPItem.version == plugin.version) { pluginStatus = 'installed'; } else { pluginStatus = 'update'; } break; } } return pluginStatus; } String pluginUpdateStatus(Plugin plugin) { if (!pluginHTTPList.any((p) => p.name == plugin.name)) { return "nonexistent"; } PluginHTTPItem p = pluginHTTPList.firstWhere( (p) => p.name == plugin.name, ); return p.version == plugin.version ? "latest" : "updatable"; } Future tryUpdatePlugin(Plugin plugin) async { return await tryUpdatePluginByName(plugin.name); } Future tryUpdatePluginByName(String name) async { var pluginHTTPItem = await queryPluginHTTP(name); if (pluginHTTPItem != null) { if (int.parse(pluginHTTPItem.api) > Api.apiLevel) { return 1; } updatePlugin(pluginHTTPItem); return 0; } return 2; } Future tryUpdateAllPlugin() async { int count = 0; for (Plugin plugin in pluginList) { if (pluginUpdateStatus(plugin) == 'updatable') { if (await tryUpdatePlugin(plugin) == 0) { count++; } } } return count; } void removePlugins(Set pluginNames) { for (int i = pluginList.length - 1; i >= 0; --i) { var name = pluginList[i].name; if (pluginNames.contains(name)) { pluginList.removeAt(i); } } savePlugins(); } } ================================================ FILE: lib/plugins/plugins_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'plugins_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$PluginsController on _PluginsController, Store { late final _$pluginListAtom = Atom(name: '_PluginsController.pluginList', context: context); @override ObservableList get pluginList { _$pluginListAtom.reportRead(); return super.pluginList; } @override set pluginList(ObservableList value) { _$pluginListAtom.reportWrite(value, super.pluginList, () { super.pluginList = value; }); } late final _$pluginHTTPListAtom = Atom(name: '_PluginsController.pluginHTTPList', context: context); @override ObservableList get pluginHTTPList { _$pluginHTTPListAtom.reportRead(); return super.pluginHTTPList; } @override set pluginHTTPList(ObservableList value) { _$pluginHTTPListAtom.reportWrite(value, super.pluginHTTPList, () { super.pluginHTTPList = value; }); } @override String toString() { return ''' pluginList: ${pluginList}, pluginHTTPList: ${pluginHTTPList} '''; } } ================================================ FILE: lib/providers/captcha/captcha_provider.dart ================================================ import 'dart:async'; import 'package:kazumi/webview/captcha/captcha_webview_controller.dart'; import 'package:kazumi/plugins/plugin_cookie_manager.dart'; import 'package:kazumi/utils/logger.dart'; /// 验证码解决 Provider /// /// 支持两种独立的验证流程: /// /// **类型1:图片验证码**([loadForCaptcha] + [submitCaptcha]) /// 1. 初始化 WebView /// 2. 加载搜索页面,注入 JS 脚本监听验证码图片 /// 3. 通过 [onCaptchaImageUrl] 流将验证码图片 URL 暴露给 UI /// 4. UI 将用户输入的验证码传给 [submitCaptcha] /// 5. 页面通过 AJAX 提交验证码 /// 6. 验证码图片消失后,获取页面 Cookie 并保存到 [PluginCookieManager] /// 7. UI 发起重新检索 /// /// **类型2:自动点击验证按钮**([loadForButtonClick]) /// 1. 初始化 WebView /// 2. 加载搜索页面,注入 JS 脚本轮询验证按钮 /// 3. 检测到按钮后自动模拟点击 /// 4. 按钮消失时,获取页面 Cookie 并保存到 [PluginCookieManager] /// 5. 通过 [onVerified] 回调通知 UI 发起重新检索 class CaptchaProvider { CaptchaWebviewController? _controller; final StreamController _captchaImageStreamController = StreamController.broadcast(); Stream get onCaptchaImageUrl => _captchaImageStreamController.stream; StreamSubscription? _imageFoundSub; StreamSubscription? _disappearedSub; StreamSubscription? _logSub; bool _isInitialized = false; bool _disposed = false; String _pageUrl = ''; Future _ensureInitialized() async { if (_isInitialized || _disposed) return; _controller = CaptchaWebviewControllerFactory.getController(); final initializedFuture = _controller!.onInitialized.first .timeout(const Duration(seconds: 10), onTimeout: () => false); await _controller!.init(); if (_disposed) return; await initializedFuture; if (_disposed) return; _logSub?.cancel(); _logSub = _controller!.onLog.listen((msg) => KazumiLogger().d(msg)); _isInitialized = true; KazumiLogger().i('[CaptchaProvider] WebView initialized'); } /// 加载指定页面并开始监听验证码图片 /// /// [url] 要加载的页面地址 /// [captchaXpath] 验证码图片元素的 XPath /// [inputXpath] 可选,验证码输入框的 XPath。如果提供,会在检测验证码前先触发输入框的 focus 事件 Future loadForCaptcha(String url, String captchaXpath, {String? inputXpath}) async { _pageUrl = url; await _ensureInitialized(); if (_disposed || _controller == null) return; _imageFoundSub?.cancel(); _imageFoundSub = _controller!.onCaptchaImageFound.listen((src) { KazumiLogger().i('[CaptchaProvider] Captcha image found: $src'); if (!_captchaImageStreamController.isClosed) { _captchaImageStreamController.add(src); } }); await _controller!.loadPage(url, captchaXpath, inputXpath: inputXpath); KazumiLogger().i('[CaptchaProvider] Page loading: $url'); } /// 提交验证码 /// /// [captchaCode] 用户输入的验证码文本 /// [inputXpath] 验证码输入框元素的 XPath /// [buttonXpath] 验证提交按钮元素的 XPath /// [pluginName] 规则名(用于保存 Cookie) /// [onVerified] 验证成功后的回调 Future submitCaptcha({ required String captchaCode, required String inputXpath, required String buttonXpath, required String pluginName, required void Function() onVerified, }) async { if (_controller == null) { KazumiLogger().w('[CaptchaProvider] submitCaptcha called before init'); return; } KazumiLogger().i('[CaptchaProvider] Submitting captcha code via interact'); bool _handled = false; Future onDisappeared() async { if (_handled) return; _handled = true; _disappearedSub?.cancel(); final cookieString = await _controller!.getCookieString(_pageUrl); KazumiLogger().i('[CaptchaProvider] Captured cookies: $cookieString'); if (cookieString.isNotEmpty) { await PluginCookieManager.instance .saveFromWebView(pluginName, _pageUrl, cookieString); KazumiLogger() .i('[CaptchaProvider] Cookies saved for plugin: $pluginName'); } await _controller!.unloadPage(); onVerified(); } _disappearedSub?.cancel(); _disappearedSub = _controller!.onCaptchaDisappeared.listen((_) { onDisappeared(); }); await _controller!.submitCaptchaInteract(captchaCode, inputXpath, buttonXpath); } /// 加载页面并自动点击验证按钮 /// /// [url] 要加载的页面地址 /// [buttonXpath] 验证按钮元素的 XPath,检测到后自动点击 /// [pluginName] 规则名(用于保存 Cookie) /// [onVerified] 按钮消失(验证通过)后的回调 Future loadForButtonClick({ required String url, required String buttonXpath, required String pluginName, required void Function() onVerified, }) async { _pageUrl = url; await _ensureInitialized(); if (_disposed || _controller == null) return; bool _handled = false; Future onDisappeared() async { if (_handled) return; _handled = true; _disappearedSub?.cancel(); final cookieString = await _controller!.getCookieString(_pageUrl); KazumiLogger().i('[CaptchaProvider] (type2) Captured cookies: $cookieString'); if (cookieString.isNotEmpty) { await PluginCookieManager.instance .saveFromWebView(pluginName, _pageUrl, cookieString); KazumiLogger() .i('[CaptchaProvider] (type2) Cookies saved for plugin: $pluginName'); } await _controller!.unloadPage(); onVerified(); } _disappearedSub?.cancel(); _disappearedSub = _controller!.onCaptchaDisappeared.listen((_) { onDisappeared(); }); await _controller!.loadPageForButtonClick(url, buttonXpath); KazumiLogger().i('[CaptchaProvider] (type2) Page loading for button click: $url'); } Future saveAndUnload(String pluginName) async { _disappearedSub?.cancel(); _disappearedSub = null; // Capture locally before any await so dispose() nulling _controller // between two awaits cannot cause a force-unwrap crash. final controller = _controller; if (controller == null || _pageUrl.isEmpty) return; final cookieString = await controller.getCookieString(_pageUrl); KazumiLogger() .i('[CaptchaProvider] Captured cookies on cancel: $cookieString'); if (cookieString.isNotEmpty) { await PluginCookieManager.instance .saveFromWebView(pluginName, _pageUrl, cookieString); KazumiLogger() .i('[CaptchaProvider] Cookies saved on cancel for plugin: $pluginName'); } await controller.unloadPage(); } Stream? get onLog => _controller?.onLog; void dispose() { if (_disposed) return; _disposed = true; _imageFoundSub?.cancel(); _disappearedSub?.cancel(); _logSub?.cancel(); if (!_captchaImageStreamController.isClosed) { _captchaImageStreamController.close(); } _controller?.dispose(); _controller = null; _isInitialized = false; KazumiLogger().i('[CaptchaProvider] Disposed'); } } ================================================ FILE: lib/providers/video/providers.dart ================================================ /// Video Source Provider 模块 /// /// 提供视频源解析的抽象层,支持: /// - WebView 在线解析 /// /// 使用示例: /// ```dart /// final provider = WebViewVideoSourceProvider(); /// try { /// final source = await provider.resolve( /// episodeUrl, /// useLegacyParser: false, /// ); /// print('Video URL: ${source.url}'); /// } on VideoSourceTimeoutException { /// print('解析超时'); /// } finally { /// provider.dispose(); /// } /// ``` library; export 'video_source_provider.dart'; export 'webview_video_source_provider.dart'; ================================================ FILE: lib/providers/video/video_source_provider.dart ================================================ import 'dart:async'; /// 视频源类型 enum VideoSourceType { /// 在线解析(WebView) online, /// 本地缓存 cached, } /// 视频源解析结果 class VideoSource { /// 视频 URL (M3U8/MP4/本地路径) final String url; /// 播放偏移量(秒) final int offset; /// 视频源类型 final VideoSourceType type; const VideoSource({ required this.url, required this.offset, required this.type, }); @override String toString() => 'VideoSource(url: $url, offset: $offset, type: $type)'; } /// 视频源未找到异常 class VideoSourceNotFoundException implements Exception { final String message; const VideoSourceNotFoundException([this.message = 'Video source not found']); @override String toString() => 'VideoSourceNotFoundException: $message'; } /// 视频源解析超时异常 class VideoSourceTimeoutException implements Exception { final Duration timeout; const VideoSourceTimeoutException(this.timeout); @override String toString() => 'VideoSourceTimeoutException: Timed out after ${timeout.inSeconds}s'; } /// 视频源解析取消异常 class VideoSourceCancelledException implements Exception { const VideoSourceCancelledException(); @override String toString() => 'VideoSourceCancelledException: Resolution was cancelled'; } /// 视频源提供者接口 /// /// 抽象视频源的获取方式,支持多种实现: /// - WebView 解析(在线) /// - 本地缓存读取 /// - 组合策略(优先缓存,回退 WebView) abstract class IVideoSourceProvider { /// 解析视频源 URL /// /// [episodeUrl] 集数页面 URL /// [useLegacyParser] 是否使用旧版解析器(iframe 监听) /// [offset] 播放偏移量(秒) /// [timeout] 解析超时时间 /// /// 返回 [VideoSource] 包含解析后的视频 URL 和元数据 /// /// 可能抛出: /// - [VideoSourceNotFoundException] 未找到视频源 /// - [VideoSourceTimeoutException] 解析超时 /// - [VideoSourceCancelledException] 解析被取消 Future resolve( String episodeUrl, { required bool useLegacyParser, int offset = 0, Duration timeout = const Duration(seconds: 30), }); /// 取消当前正在进行的解析 /// /// 调用后,正在进行的 [resolve] 会抛出 [VideoSourceCancelledException] void cancel(); /// 释放资源 void dispose(); } ================================================ FILE: lib/providers/video/webview_video_source_provider.dart ================================================ import 'dart:async'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:kazumi/providers/video/video_source_provider.dart'; /// WebView 视频源提供者 /// /// 使用 WebView 解析视频页面,提取视频源 URL。 /// WebView 实例在 Provider 生命周期内复用,切换集数时调用 unloadPage 释放页面资源, /// 仅在 [dispose] 时才真正销毁 WebView。 class WebViewVideoSourceProvider implements IVideoSourceProvider { VideoWebviewController? _webview; StreamSubscription? _logSubscription; /// 单个 Provider 实例不能实现并发解析,单个 Provider 实例只能持有一个 Webview /// 但是 Provider 可以在正在进行的解析未完成时,取消该解析并开始新的解析 /// 通过递增 ID 标识最新请求,取消旧请求 int _resolveId = 0; final StreamController _logController = StreamController.broadcast(); Stream get onLog => _logController.stream; @override Future resolve( String episodeUrl, { required bool useLegacyParser, int offset = 0, Duration timeout = const Duration(seconds: 15), }) async { _resolveId++; final currentResolveId = _resolveId; if (_webview == null) { _webview = VideoWebviewControllerFactory.getController(); await _webview!.init(); _logSubscription = _webview!.onLog.listen((log) { if (!_logController.isClosed) { _logController.add(log); } }); } try { await _webview!.loadUrl( episodeUrl, useLegacyParser, offset: offset, ); if (currentResolveId != _resolveId) { throw const VideoSourceCancelledException(); } final event = await _webview!.onVideoURLParser.first.timeout( timeout, onTimeout: () { if (currentResolveId != _resolveId) { throw const VideoSourceCancelledException(); } throw VideoSourceTimeoutException(timeout); }, ); if (currentResolveId != _resolveId) { throw const VideoSourceCancelledException(); } return VideoSource( url: event.$1, offset: event.$2, type: VideoSourceType.online, ); } catch (e) { if (e is VideoSourceCancelledException) { rethrow; } if (currentResolveId != _resolveId) { throw const VideoSourceCancelledException(); } rethrow; } finally { if (currentResolveId == _resolveId) { await _webview?.unloadPage(); } } } @override void cancel() { _resolveId++; } @override void dispose() { cancel(); _logSubscription?.cancel(); _logSubscription = null; _logController.close(); _webview?.dispose(); _webview = null; } } ================================================ FILE: lib/repositories/collect_crud_repository.dart ================================================ import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:kazumi/modules/collect/collect_change_module.dart'; import 'package:kazumi/utils/logger.dart'; /// 收藏CRUD数据访问接口 /// /// 提供收藏数据的增删改查操作 abstract class ICollectCrudRepository { /// 获取所有收藏 List getAllCollectibles(); /// 获取单个收藏 /// /// [id] 番剧ID /// 返回收藏对象,如果不存在返回null CollectedBangumi? getCollectible(int id); /// 获取收藏类型 /// /// [id] 番剧ID /// 返回收藏类型值,未收藏返回0 int getCollectType(int id); /// 添加或更新收藏 /// /// [bangumiItem] 番剧信息 /// [type] 收藏类型 Future addCollectible(BangumiItem bangumiItem, int type); /// 更新收藏的番剧信息 /// /// [bangumiItem] 更新后的番剧信息 Future updateCollectible(BangumiItem bangumiItem); /// 删除收藏 /// /// [id] 番剧ID Future deleteCollectible(int id); /// 记录收藏变更(用于WebDAV同步) /// /// [change] 变更记录 Future addCollectChange(CollectedBangumiChange change); /// 获取旧版收藏列表(用于迁移) List getFavorites(); /// 清空旧版收藏(迁移后) Future clearFavorites(); } /// 收藏CRUD数据访问实现类 /// /// 基于Hive实现的收藏CRUD数据访问层 class CollectCrudRepository implements ICollectCrudRepository { final _collectiblesBox = GStorage.collectibles; final _collectChangesBox = GStorage.collectChanges; final _favoritesBox = GStorage.favorites; @override List getAllCollectibles() { try { return _collectiblesBox.values.cast().toList(); } catch (e) { KazumiLogger().w( 'GStorage: get all collectibles failed', error: e, ); return []; } } @override CollectedBangumi? getCollectible(int id) { try { return _collectiblesBox.get(id); } catch (e) { KazumiLogger().w( 'GStorage: get collectible failed. id=$id', error: e, ); return null; } } @override int getCollectType(int id) { try { final collectible = _collectiblesBox.get(id); return collectible?.type ?? 0; } catch (e) { KazumiLogger().w( 'GStorage: get collect type failed. id=$id', error: e, ); return 0; } } @override Future addCollectible(BangumiItem bangumiItem, int type) async { try { final collectedBangumi = CollectedBangumi( bangumiItem, DateTime.now(), type, ); await _collectiblesBox.put(bangumiItem.id, collectedBangumi); await _collectiblesBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: add collectible failed. id=${bangumiItem.id}, type=$type', error: e, stackTrace: stackTrace, ); rethrow; } } @override Future updateCollectible(BangumiItem bangumiItem) async { try { final collectible = _collectiblesBox.get(bangumiItem.id); if (collectible == null) { KazumiLogger().i( 'GStorage: update collectible failed. collectible not found, id=${bangumiItem.id}', ); return; } collectible.bangumiItem = bangumiItem; await _collectiblesBox.put(bangumiItem.id, collectible); await _collectiblesBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update collectible failed. id=${bangumiItem.id}', error: e, stackTrace: stackTrace, ); rethrow; } } @override Future deleteCollectible(int id) async { try { await _collectiblesBox.delete(id); await _collectiblesBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: delete collectible failed. id=$id', error: e, stackTrace: stackTrace, ); rethrow; } } @override Future addCollectChange(CollectedBangumiChange change) async { try { await _collectChangesBox.put(change.id, change); await _collectChangesBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: record collect change failed. changeId=${change.id}', error: e, stackTrace: stackTrace, ); rethrow; } } @override List getFavorites() { try { return _favoritesBox.values.cast().toList(); } catch (e) { KazumiLogger().i( 'GStorage: get favorites failed', error: e, ); return []; } } @override Future clearFavorites() async { try { await _favoritesBox.clear(); await _favoritesBox.flush(); } catch (e) { KazumiLogger().i( 'GStorage: clear favorites failed', error: e, ); rethrow; } } } ================================================ FILE: lib/repositories/collect_repository.dart ================================================ import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/modules/collect/collect_type.dart'; import 'package:kazumi/utils/logger.dart'; /// 收藏数据访问接口 /// /// 提供收藏相关的数据访问抽象,解耦业务逻辑与数据存储 abstract class ICollectRepository { /// 根据收藏类型获取番剧ID集合 /// /// [type] 收藏类型 /// 返回符合条件的番剧ID集合 Set getBangumiIdsByType(CollectType type); /// 批量获取多种类型的番剧ID集合 /// /// [types] 收藏类型列表 /// 返回符合条件的番剧ID集合(并集) Set getBangumiIdsByTypes(List types); // ========== 搜索页过滤器设置 ========== /// 获取搜索页"不显示已看过番剧"设置 bool getSearchNotShowWatchedBangumis(); /// 更新搜索页"不显示已看过番剧"设置 Future updateSearchNotShowWatchedBangumis(bool value); /// 获取搜索页"不显示已抛弃番剧"设置 bool getSearchNotShowAbandonedBangumis(); /// 更新搜索页"不显示已抛弃番剧"设置 Future updateSearchNotShowAbandonedBangumis(bool value); // ========== 时间表页过滤器设置 ========== /// 获取时间表页"不显示已抛弃番剧"设置 bool getTimelineNotShowAbandonedBangumis(); /// 更新时间表页"不显示已抛弃番剧"设置 Future updateTimelineNotShowAbandonedBangumis(bool value); /// 获取时间表页"不显示已看过番剧"设置 bool getTimelineNotShowWatchedBangumis(); /// 更新时间表页"不显示已看过番剧"设置 Future updateTimelineNotShowWatchedBangumis(bool value); // ========== 其他设置 ========== /// 获取隐私模式设置 bool getPrivateMode(); } /// 收藏数据访问实现类 /// /// 基于Hive实现的收藏数据访问层 class CollectRepository implements ICollectRepository { final _collectiblesBox = GStorage.collectibles; final _settingBox = GStorage.setting; @override Set getBangumiIdsByType(CollectType type) { try { return _collectiblesBox.values .where((item) => item.type == type.value) .map((item) => item.bangumiItem.id) .toSet(); } catch (e) { KazumiLogger().w( 'GStorage: get bangumi IDs by type failed. type=${type.label}', error: e, ); return {}; } } @override Set getBangumiIdsByTypes(List types) { try { final typeValues = types.map((t) => t.value).toSet(); return _collectiblesBox.values .where((item) => typeValues.contains(item.type)) .map((item) => item.bangumiItem.id) .toSet(); } catch (e) { KazumiLogger().w( 'GStorage: get bangumi IDs by types failed. types=${types.map((t) => t.label).join(", ")}', error: e, ); return {}; } } // ========== 搜索页过滤器设置实现 ========== @override bool getSearchNotShowWatchedBangumis() { try { final value = _settingBox.get( SettingBoxKey.searchNotShowWatchedBangumis, defaultValue: false, ); return value is bool ? value : false; } catch (e) { KazumiLogger().w( 'GStorage: get search not show watched bangumis setting failed, using default false', error: e, ); return false; } } @override Future updateSearchNotShowWatchedBangumis(bool value) async { try { await _settingBox.put(SettingBoxKey.searchNotShowWatchedBangumis, value); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update search not show watched bangumis setting failed. value=$value', error: e, stackTrace: stackTrace, ); } } @override bool getSearchNotShowAbandonedBangumis() { try { final value = _settingBox.get( SettingBoxKey.searchNotShowAbandonedBangumis, defaultValue: false, ); return value is bool ? value : false; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get search not show abandoned bangumis setting failed, using default false', error: e, stackTrace: stackTrace, ); return false; } } @override Future updateSearchNotShowAbandonedBangumis(bool value) async { try { await _settingBox.put(SettingBoxKey.searchNotShowAbandonedBangumis, value); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update search not show abandoned bangumis setting failed. value=$value', error: e, stackTrace: stackTrace, ); } } // ========== 时间表页过滤器设置实现 ========== @override bool getTimelineNotShowAbandonedBangumis() { try { final value = _settingBox.get( SettingBoxKey.timelineNotShowAbandonedBangumis, defaultValue: false, ); return value is bool ? value : false; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get timeline not show abandoned bangumis setting failed, using default false', error: e, stackTrace: stackTrace, ); return false; } } @override Future updateTimelineNotShowAbandonedBangumis(bool value) async { try { await _settingBox.put(SettingBoxKey.timelineNotShowAbandonedBangumis, value); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update timeline not show abandoned bangumis setting failed. value=$value', error: e, stackTrace: stackTrace, ); } } @override bool getTimelineNotShowWatchedBangumis() { try { final value = _settingBox.get( SettingBoxKey.timelineNotShowWatchedBangumis, defaultValue: false, ); return value is bool ? value : false; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get timeline not show watched bangumis setting failed, using default false', error: e, stackTrace: stackTrace, ); return false; } } @override Future updateTimelineNotShowWatchedBangumis(bool value) async { try { await _settingBox.put(SettingBoxKey.timelineNotShowWatchedBangumis, value); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update timeline not show watched bangumis setting failed. value=$value', error: e, stackTrace: stackTrace, ); } } // ========== 其他设置实现 ========== @override bool getPrivateMode() { try { final value = _settingBox.get( SettingBoxKey.privateMode, defaultValue: false, ); return value is bool ? value : false; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get private mode setting failed, using default false', error: e, stackTrace: stackTrace, ); return false; } } } ================================================ FILE: lib/repositories/download_repository.dart ================================================ import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/utils/logger.dart'; abstract class IDownloadRepository { List getAllRecords(); DownloadRecord? getRecord(String key); Future putRecord(DownloadRecord record); Future deleteRecord(String key); Future updateEpisode(String recordKey, int episodeNumber, DownloadEpisode episode); Future deleteEpisode(String recordKey, int episodeNumber); bool getForceAdBlocker(); /// 获取指定番剧的下载记录 /// /// [bangumiId] 番剧 ID /// [pluginName] 插件名称 DownloadRecord? getRecordByBangumiId(int bangumiId, String pluginName); /// 获取指定集数的下载信息 /// /// [bangumiId] 番剧 ID /// [pluginName] 插件名称 /// [episodeNumber] 集数编号 DownloadEpisode? getEpisode(int bangumiId, String pluginName, int episodeNumber); /// 获取已完成下载的集数列表 /// /// [bangumiId] 番剧 ID /// [pluginName] 插件名称 /// 返回所有已完成下载的集数 List getCompletedEpisodes(int bangumiId, String pluginName); /// 通过集数页面 URL 查找下载记录 /// /// [bangumiId] 番剧 ID /// [pluginName] 插件名称 /// [episodePageUrl] 集数页面 URL /// 当 URL 为空时返回 null(兼容旧数据) DownloadEpisode? getEpisodeByUrl(int bangumiId, String pluginName, String episodePageUrl); } class DownloadRepository implements IDownloadRepository { final _downloadsBox = GStorage.downloads; @override List getAllRecords() { final List result = []; try { for (final key in _downloadsBox.keys) { try { final record = _downloadsBox.get(key); if (record != null) { // Merge in-memory progress into the record final cachedEpisodes = _progressCache[key as String]; if (cachedEpisodes != null) { for (final entry in cachedEpisodes.entries) { record.episodes[entry.key] = entry.value; } } result.add(record); } } catch (e) { // 单条记录读取失败,跳过该记录并记录日志 KazumiLogger().w('DownloadRepository: failed to read record key=$key, skipping', error: e); } } } catch (e) { KazumiLogger().w('DownloadRepository: get all records failed', error: e); } return result; } @override DownloadRecord? getRecord(String key) { try { final record = _downloadsBox.get(key); if (record != null) { // Merge in-memory progress into the record final cachedEpisodes = _progressCache[key]; if (cachedEpisodes != null) { for (final entry in cachedEpisodes.entries) { record.episodes[entry.key] = entry.value; } } } return record; } catch (e) { KazumiLogger().w('DownloadRepository: get record failed. key=$key', error: e); return null; } } @override Future putRecord(DownloadRecord record) async { try { await _downloadsBox.put(record.key, record); await _downloadsBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'DownloadRepository: put record failed. key=${record.key}', error: e, stackTrace: stackTrace, ); rethrow; } } @override Future deleteRecord(String key) async { try { await _downloadsBox.delete(key); await _downloadsBox.flush(); _progressCache.remove(key); _lastPersistedStatus.removeWhere((k, v) => k.startsWith('${key}_')); } catch (e, stackTrace) { KazumiLogger().e( 'DownloadRepository: delete record failed. key=$key', error: e, stackTrace: stackTrace, ); rethrow; } } /// Track last persisted status to avoid unnecessary writes final Map _lastPersistedStatus = {}; /// In-memory cache for progress updates (not persisted until status changes) final Map> _progressCache = {}; @override Future updateEpisode(String recordKey, int episodeNumber, DownloadEpisode episode) async { try { // Update in-memory cache _progressCache.putIfAbsent(recordKey, () => {}); _progressCache[recordKey]![episodeNumber] = episode; // Only persist to Hive when status changes (not on every progress update) // This dramatically reduces disk I/O and prevents corruption on crash final statusKey = '${recordKey}_$episodeNumber'; final lastStatus = _lastPersistedStatus[statusKey]; final shouldPersist = lastStatus != episode.status; if (shouldPersist) { final record = _downloadsBox.get(recordKey); if (record == null) return; record.episodes[episodeNumber] = episode; await _downloadsBox.put(recordKey, record); await _downloadsBox.flush(); _lastPersistedStatus[statusKey] = episode.status; } } catch (e, stackTrace) { KazumiLogger().e( 'DownloadRepository: update episode failed. key=$recordKey, ep=$episodeNumber', error: e, stackTrace: stackTrace, ); rethrow; } } /// Get episode with in-memory progress if available DownloadEpisode? getEpisodeWithProgress(String recordKey, int episodeNumber) { // Check in-memory cache first final cached = _progressCache[recordKey]?[episodeNumber]; if (cached != null) return cached; // Fall back to Hive final record = getRecord(recordKey); return record?.episodes[episodeNumber]; } @override bool getForceAdBlocker() { return GStorage.setting.get(SettingBoxKey.forceAdBlocker, defaultValue: false); } @override Future deleteEpisode(String recordKey, int episodeNumber) async { try { final record = _downloadsBox.get(recordKey); if (record == null) return; record.episodes.remove(episodeNumber); if (record.episodes.isEmpty) { await _downloadsBox.delete(recordKey); _progressCache.remove(recordKey); _lastPersistedStatus.removeWhere((k, v) => k.startsWith('${recordKey}_')); } else { await _downloadsBox.put(recordKey, record); _progressCache[recordKey]?.remove(episodeNumber); _lastPersistedStatus.remove('${recordKey}_$episodeNumber'); } await _downloadsBox.flush(); } catch (e, stackTrace) { KazumiLogger().e( 'DownloadRepository: delete episode failed. key=$recordKey, ep=$episodeNumber', error: e, stackTrace: stackTrace, ); rethrow; } } @override DownloadRecord? getRecordByBangumiId(int bangumiId, String pluginName) { final key = '${pluginName}_$bangumiId'; return getRecord(key); } @override DownloadEpisode? getEpisode(int bangumiId, String pluginName, int episodeNumber) { final record = getRecordByBangumiId(bangumiId, pluginName); return record?.episodes[episodeNumber]; } @override List getCompletedEpisodes(int bangumiId, String pluginName) { final record = getRecordByBangumiId(bangumiId, pluginName); if (record == null) return []; return record.episodes.values .where((e) => e.status == DownloadStatus.completed) .toList() ..sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber)); } @override DownloadEpisode? getEpisodeByUrl(int bangumiId, String pluginName, String episodePageUrl) { if (episodePageUrl.isEmpty) return null; final record = getRecordByBangumiId(bangumiId, pluginName); if (record == null) return null; for (final episode in record.episodes.values) { if (episode.episodePageUrl == episodePageUrl) { return episode; } } return null; } } ================================================ FILE: lib/repositories/history_repository.dart ================================================ import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/utils/logger.dart'; /// 历史记录数据访问接口 /// /// 提供观看历史相关的数据访问抽象 abstract class IHistoryRepository { /// 获取所有历史记录(按时间倒序) List getAllHistories(); /// 获取特定番剧的历史记录 /// /// [adapterName] 适配器名称 /// [bangumiItem] 番剧信息 /// 返回历史记录,不存在返回null History? getHistory(String adapterName, BangumiItem bangumiItem); /// 更新或创建历史记录 /// /// [episode] 集数 /// [road] 线路 /// [adapterName] 适配器名称 /// [bangumiItem] 番剧信息 /// [progress] 观看进度 /// [lastSrc] 最后观看源 /// [lastWatchEpisodeName] 最后观看集名称 Future updateHistory({ required int episode, required int road, required String adapterName, required BangumiItem bangumiItem, required Duration progress, required String lastSrc, required String lastWatchEpisodeName, }); /// 获取上次观看的进度 /// /// [bangumiItem] 番剧信息 /// [adapterName] 适配器名称 /// 返回观看进度,不存在返回null Progress? getLastWatchingProgress(BangumiItem bangumiItem, String adapterName); /// 查找特定集数的观看进度 /// /// [bangumiItem] 番剧信息 /// [adapterName] 适配器名称 /// [episode] 集数 /// 返回观看进度,不存在返回null Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode); /// 删除历史记录 /// /// [history] 要删除的历史记录 Future deleteHistory(History history); /// 清空特定集数的观看进度 /// /// [bangumiItem] 番剧信息 /// [adapterName] 适配器名称 /// [episode] 集数 Future clearProgress(BangumiItem bangumiItem, String adapterName, int episode); /// 清空所有历史记录 Future clearAllHistories(); /// 获取隐私模式设置 bool getPrivateMode(); } /// 历史记录数据访问实现类 /// /// 基于Hive实现的历史记录数据访问层 class HistoryRepository implements IHistoryRepository { final _historiesBox = GStorage.histories; final _settingBox = GStorage.setting; @override List getAllHistories() { try { var histories = _historiesBox.values.toList(); histories.sort( (a, b) => b.lastWatchTime.millisecondsSinceEpoch - a.lastWatchTime.millisecondsSinceEpoch, ); return histories; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get all histories failed', error: e, stackTrace: stackTrace, ); return []; } } @override History? getHistory(String adapterName, BangumiItem bangumiItem) { try { return _historiesBox.get(History.getKey(adapterName, bangumiItem)); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get history failed. bangumi=${bangumiItem.name}', error: e, stackTrace: stackTrace, ); return null; } } @override Future updateHistory({ required int episode, required int road, required String adapterName, required BangumiItem bangumiItem, required Duration progress, required String lastSrc, required String lastWatchEpisodeName, }) async { try { // 检查隐私模式 if (getPrivateMode()) { return; } // 获取或创建历史记录 var history = _historiesBox.get(History.getKey(adapterName, bangumiItem)) ?? History(bangumiItem, episode, adapterName, DateTime.now(), lastSrc, lastWatchEpisodeName); // 更新历史记录 history.lastWatchEpisode = episode; history.lastWatchTime = DateTime.now(); if (lastSrc.isNotEmpty) { history.lastSrc = lastSrc; } if (lastWatchEpisodeName.isNotEmpty) { history.lastWatchEpisodeName = lastWatchEpisodeName; } // 更新观看进度 var prog = history.progresses[episode]; if (prog == null) { history.progresses[episode] = Progress(episode, road, progress.inMilliseconds); } else { prog.progress = progress; } // 保存到存储 await _historiesBox.put(history.key, history); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: update history failed. bangumi=${bangumiItem.name}, episode=$episode', error: e, stackTrace: stackTrace, ); } } @override Progress? getLastWatchingProgress(BangumiItem bangumiItem, String adapterName) { try { var history = _historiesBox.get(History.getKey(adapterName, bangumiItem)); return history?.progresses[history.lastWatchEpisode]; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get last watching progress failed. bangumi=${bangumiItem.name}', error: e, stackTrace: stackTrace, ); return null; } } @override Progress? findProgress(BangumiItem bangumiItem, String adapterName, int episode) { try { var history = _historiesBox.get(History.getKey(adapterName, bangumiItem)); return history?.progresses[episode]; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: find progress failed. bangumi=${bangumiItem.name}, episode=$episode', error: e, stackTrace: stackTrace, ); return null; } } @override Future deleteHistory(History history) async { try { await _historiesBox.delete(history.key); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: delete history failed. bangumi=${history.bangumiItem.name}', error: e, stackTrace: stackTrace, ); } } @override Future clearProgress(BangumiItem bangumiItem, String adapterName, int episode) async { try { var history = _historiesBox.get(History.getKey(adapterName, bangumiItem)); if (history != null && history.progresses[episode] != null) { history.progresses[episode]!.progress = Duration.zero; await _historiesBox.put(history.key, history); } } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: clear progress failed. bangumi=${bangumiItem.name}, episode=$episode', error: e, stackTrace: stackTrace, ); } } @override Future clearAllHistories() async { try { await _historiesBox.clear(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: clear all histories failed', error: e, stackTrace: stackTrace, ); } } @override bool getPrivateMode() { try { final value = _settingBox.get( SettingBoxKey.privateMode, defaultValue: false, ); return value is bool ? value : false; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get private mode setting failed, using default false', error: e, stackTrace: stackTrace, ); return false; } } } ================================================ FILE: lib/repositories/search_history_repository.dart ================================================ import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/modules/search/search_history_module.dart'; import 'package:kazumi/utils/logger.dart'; /// 搜索历史数据访问接口 /// /// 提供搜索历史相关的数据访问抽象 abstract class ISearchHistoryRepository { /// 获取所有搜索历史(按时间戳倒序) List getAllHistories(); /// 保存搜索历史 /// /// [keyword] 搜索关键词 /// 返回是否成功 Future saveHistory(String keyword); /// 删除指定搜索历史 /// /// [history] 要删除的历史记录 Future deleteHistory(SearchHistory history); /// 清空所有搜索历史 Future clearAllHistories(); /// 删除重复的历史记录 /// /// [keyword] 关键词 Future deleteDuplicates(String keyword); /// 检查是否达到最大历史记录数 /// /// [maxCount] 最大记录数 /// 返回是否已满 bool isHistoryFull(int maxCount); /// 删除最旧的历史记录 Future deleteOldest(); } /// 搜索历史数据访问实现类 /// /// 基于Hive实现的搜索历史数据访问层 class SearchHistoryRepository implements ISearchHistoryRepository { final _searchHistoryBox = GStorage.searchHistory; @override List getAllHistories() { try { final histories = _searchHistoryBox.values.toList().cast(); histories.sort((a, b) => b.timestamp - a.timestamp); return histories; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: get all search histories failed', error: e, stackTrace: stackTrace, ); return []; } } @override Future saveHistory(String keyword) async { try { final timestamp = DateTime.now().millisecondsSinceEpoch; final history = SearchHistory(keyword, timestamp); await _searchHistoryBox.put(timestamp.toString(), history); return true; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: save search history failed. keyword=$keyword', error: e, stackTrace: stackTrace, ); return false; } } @override Future deleteHistory(SearchHistory history) async { try { await _searchHistoryBox.delete(history.key); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: delete search history failed. key=${history.key}', error: e, stackTrace: stackTrace, ); } } @override Future clearAllHistories() async { try { await _searchHistoryBox.clear(); } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: clear all search histories failed', error: e, stackTrace: stackTrace, ); } } @override Future deleteDuplicates(String keyword) async { try { final histories = getAllHistories(); final duplicates = histories.where((h) => h.keyword == keyword); for (var history in duplicates) { await deleteHistory(history); } } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: delete duplicate search histories failed. keyword=$keyword', error: e, stackTrace: stackTrace, ); } } @override bool isHistoryFull(int maxCount) { try { return _searchHistoryBox.length >= maxCount; } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: check if search history is full failed', error: e, stackTrace: stackTrace, ); return false; } } @override Future deleteOldest() async { try { final histories = getAllHistories(); if (histories.isNotEmpty) { await deleteHistory(histories.last); } } catch (e, stackTrace) { KazumiLogger().e( 'GStorage: delete oldest search history failed', error: e, stackTrace: stackTrace, ); } } } ================================================ FILE: lib/request/api.dart ================================================ class Api { /// 当前版本 static const String version = '2.0.5'; /// 规则API级别 static const int apiLevel = 6; /// 项目主页 static const String projectUrl = "https://kazumi.app/"; /// Github 项目主页 static const String sourceUrl = "https://github.com/Predidit/Kazumi"; /// 图标作者 static const String iconUrl = "https://www.pixiv.net/users/66219277"; /// 规则仓库 static const String pluginShop = 'https://raw.githubusercontent.com/Predidit/KazumiRules/main/'; /// 在线升级 static const String latestApp = 'https://api.github.com/repos/Predidit/Kazumi/releases/latest'; /// Github镜像 static const String gitMirror = 'https://ghfast.top/'; /// 弹弹官网 static const String dandanIndex = 'https://www.dandanplay.com/'; /// Bangumi 官网 static const String bangumiIndex = 'https://bangumi.tv/'; /// bangumi API Domain static const String bangumiAPIDomain = 'https://api.bgm.tv'; /// 番剧信息 static const String bangumiInfoByID = '/v0/subjects/{0}'; /// 条目搜索 static const String bangumiRankSearch = '/v0/search/subjects?limit={0}&offset={1}'; /// 从条目ID获取角色信息 static const String bangumiCharacterByID = '/v0/subjects/{0}/characters'; /// 从条目ID获取剧集ID static const String bangumiEpisodeByID = '/v0/episodes'; /// Bangumi Next API Domain static const String bangumiAPINextDomain = 'https://next.bgm.tv'; /// 每日放送 static const String bangumiCalendar = '/p1/calendar'; /// 番剧趋势 static const String bangumiTrendsNext = '/p1/trending/subjects'; /// 番剧信息 static const String bangumiInfoByIDNext = '/p1/subjects/{0}'; /// 番剧评论 static const String bangumiCommentsByIDNext = '/p1/subjects/{0}/comments?limit={1}&offset={2}'; /// 番剧剧集评论 static const String bangumiEpisodeCommentsByIDNext = '/p1/episodes/{0}/comments'; /// 番剧角色信息 static const String bangumiCharacterInfoByCharacterIDNext = '/p1/characters/{0}'; /// 番剧角色评论 static const String bangumiCharacterCommentsByIDNext = '/p1/characters/{0}/comments'; /// 番剧工作人员信息 static const String bangumiStaffByIDNext = '/p1/subjects/{0}/staffs/persons'; /// DanDanPlay API Domain static const String dandanAPIDomain = 'https://api.dandanplay.net'; /// 获取弹幕 static const String dandanAPIComment = "/api/v2/comment/"; /// 检索弹弹番剧元数据 static const String dandanAPISearch = "/api/v2/search/anime"; /// 获取弹弹番剧元数据 static const String dandanAPIInfo = "/api/v2/bangumi/"; /// 获取弹弹番剧元数据(通过BGM番剧ID) static const String dandanAPIInfoByBgmBangumiId = "/api/v2/bangumi/bgmtv/{0}"; static String formatUrl(String url, List params) { for (int i = 0; i < params.length; i++) { url = url.replaceAll('{$i}', params[i].toString()); } return url; } } ================================================ FILE: lib/request/bangumi.dart ================================================ import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/request/request.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/comments/comment_response.dart'; import 'package:kazumi/modules/characters/characters_response.dart'; import 'package:kazumi/modules/bangumi/episode_item.dart'; import 'package:kazumi/modules/character/character_full_item.dart'; import 'package:kazumi/modules/staff/staff_response.dart'; class BangumiHTTP { // why the api havn't been replaced by getCalendarBySearch? // Because getCalendarBySearch is not stable, it will miss some bangumi items. static Future>> getCalendar() async { List> bangumiCalendar = []; try { var res = await Request().get( Api.bangumiAPINextDomain + Api.bangumiCalendar, ); final jsonData = res.data; for (int i = 1; i <= 7; i++) { List bangumiList = []; final jsonList = jsonData['$i']; for (dynamic jsonItem in jsonList) { try { BangumiItem bangumiItem = BangumiItem.fromJson(jsonItem['subject']); bangumiList.add(bangumiItem); } catch (_) {} } bangumiCalendar.add(bangumiList); } } catch (e) { KazumiLogger() .e('Resolve calendar failed', error: e); } return bangumiCalendar; } // 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"] // because the air date is the launch date of the anime, it is usually a few days before the start of the season // So we usually use the start of the season month -1 and the end of the season month -1 static Future>> getCalendarBySearch( List dateRange, int limit, int offset) async { List bangumiList = []; List> bangumiCalendar = []; var params = { "keyword": "", "sort": "rank", "filter": { "type": [2], "tag": ["日本"], "air_date": [">=${dateRange[0]}", "<${dateRange[1]}"], "rank": [">0", "<=99999"], "nsfw": true } }; try { final url = Api.formatUrl( Api.bangumiAPIDomain + Api.bangumiRankSearch, [limit, offset]); final res = await Request().post( url, data: params, ); final jsonData = res.data; final jsonList = jsonData['data']; for (dynamic jsonItem in jsonList) { if (jsonItem is Map) { bangumiList.add(BangumiItem.fromJson(jsonItem)); } } } catch (e) { KazumiLogger() .e('Resolve bangumi list failed', error: e); } try { for (int weekday = 1; weekday <= 7; weekday++) { List bangumiDayList = []; for (BangumiItem bangumiItem in bangumiList) { if (bangumiItem.airWeekday == weekday) { bangumiDayList.add(bangumiItem); } } bangumiCalendar.add(bangumiDayList); } } catch (e) { KazumiLogger().e('Network: fetch bangumi item to calendar failed', error: e); } return bangumiCalendar; } static Future> getBangumiList( {int rank = 2, String tag = ''}) async { List bangumiList = []; late Map params; if (tag == '') { params = { 'keyword': '', 'sort': 'rank', "filter": { "type": [2], "tag": ["日本"], "rank": [">$rank", "<=1050"], "nsfw": false }, }; } else { params = { 'keyword': '', 'sort': 'rank', "filter": { "type": [2], "tag": [tag], "rank": [">$rank", "<=99999"], "nsfw": false }, }; } try { final res = await Request().post( Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiRankSearch, [100, 0]), data: params, ); final jsonData = res.data; final jsonList = jsonData['data']; for (dynamic jsonItem in jsonList) { if (jsonItem is Map) { bangumiList.add(BangumiItem.fromJson(jsonItem)); } } } catch (e) { KazumiLogger() .e('Network: resolve bangumi list failed', error: e); } return bangumiList; } static Future> getBangumiTrendsList( {int type = 2, int limit = 24, int offset = 0}) async { List bangumiList = []; var params = { 'type': type, 'limit': limit, 'offset': offset, }; try { final res = await Request().get( Api.bangumiAPINextDomain + Api.bangumiTrendsNext, data: params, ); final jsonData = res.data; final jsonList = jsonData['data']; for (dynamic jsonItem in jsonList) { if (jsonItem is Map) { bangumiList.add(BangumiItem.fromJson(jsonItem['subject'])); } } } catch (e) { KazumiLogger().e('Network: resolve bangumi trends list failed', error: e); } return bangumiList; } static Future> bangumiSearch(String keyword, {List tags = const [], int offset = 0, String sort = 'heat'}) async { List bangumiList = []; var params = { 'keyword': keyword, 'sort': sort, "filter": { "type": [2], "tag": tags, "rank": (sort == 'rank') ? [">0", "<=99999"] : [">=0", "<=99999"], "nsfw": false }, }; try { final res = await Request().post( Api.formatUrl( Api.bangumiAPIDomain + Api.bangumiRankSearch, [20, offset]), data: params, ); final jsonData = res.data; final jsonList = jsonData['data']; for (dynamic jsonItem in jsonList) { if (jsonItem is Map) { try { BangumiItem bangumiItem = BangumiItem.fromJson(jsonItem); if (bangumiItem.nameCn != '') { bangumiList.add(bangumiItem); } } catch (e) { KazumiLogger().e('Network: resolve search results failed', error: e); } } } } catch (e) { KazumiLogger().e('Network: unknown search problem', error: e); } return bangumiList; } static Future getBangumiInfoByID(int id) async { try { final res = await Request().get( Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiInfoByID, [id]), ); return BangumiItem.fromJson(res.data); } catch (e) { KazumiLogger().e('Network: resolve bangumi item failed', error: e); return null; } } static Future getBangumiEpisodeByID(int id, int episode) async { EpisodeInfo episodeInfo = EpisodeInfo.fromTemplate(); var params = { 'subject_id': id, 'offset': episode - 1, 'limit': 1 }; try { final res = await Request().get( Api.bangumiAPIDomain + Api.bangumiEpisodeByID, data: params, ); final jsonData = res.data['data'][0]; episodeInfo = EpisodeInfo.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi episode failed', error: e); } return episodeInfo; } static Future getBangumiCommentsByID(int id, {int offset = 0}) async { CommentResponse commentResponse = CommentResponse.fromTemplate(); try { final res = await Request().get( Api.formatUrl(Api.bangumiAPINextDomain + Api.bangumiCommentsByIDNext, [id, 20, offset]), ); final jsonData = res.data; commentResponse = CommentResponse.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi comments failed', error: e); } return commentResponse; } static Future getBangumiCommentsByEpisodeID( int id) async { EpisodeCommentResponse commentResponse = EpisodeCommentResponse.fromTemplate(); try { final res = await Request().get( Api.formatUrl( Api.bangumiAPINextDomain + Api.bangumiEpisodeCommentsByIDNext, [id]), ); final jsonData = res.data; commentResponse = EpisodeCommentResponse.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi episode comments failed', error: e); } return commentResponse; } static Future getCharacterCommentsByCharacterID( int id) async { CharacterCommentResponse commentResponse = CharacterCommentResponse.fromTemplate(); try { final res = await Request().get( Api.formatUrl( Api.bangumiAPINextDomain + Api.bangumiCharacterCommentsByIDNext, [id]), ); final jsonData = res.data; commentResponse = CharacterCommentResponse.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi character comments failed', error: e); } return commentResponse; } static Future getBangumiStaffByID(int id) async { StaffResponse staffResponse = StaffResponse.fromTemplate(); try { final res = await Request().get( Api.formatUrl( Api.bangumiAPINextDomain + Api.bangumiStaffByIDNext, [id]), ); final jsonData = res.data; staffResponse = StaffResponse.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi staff failed', error: e); } return staffResponse; } static Future getCharatersByBangumiID(int id) async { CharactersResponse charactersResponse = CharactersResponse.fromTemplate(); try { final res = await Request().get( Api.formatUrl(Api.bangumiAPIDomain + Api.bangumiCharacterByID, [id]), ); final jsonData = res.data; charactersResponse = CharactersResponse.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve bangumi characters failed', error: e); } return charactersResponse; } static Future getCharacterByCharacterID(int id) async { CharacterFullItem characterFullItem = CharacterFullItem.fromTemplate(); try { final res = await Request().get( Api.formatUrl( Api.bangumiAPINextDomain + Api.bangumiCharacterInfoByCharacterIDNext, [id]), ); final jsonData = res.data; characterFullItem = CharacterFullItem.fromJson(jsonData); } catch (e) { KazumiLogger().e('Network: resolve character info failed', error: e); } return characterFullItem; } } ================================================ FILE: lib/request/damaku.dart ================================================ import 'package:kazumi/request/request.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/modules/danmaku/danmaku_module.dart'; import 'package:kazumi/modules/danmaku/danmaku_search_response.dart'; import 'package:kazumi/modules/danmaku/danmaku_episode_response.dart'; import 'package:kazumi/utils/string_match.dart'; class DanmakuRequest { // 从BgmBangumiID获取DanDanBangumiID static Future getDanDanBangumiIDByBgmBangumiID(int bgmBangumiID) async { var path = Api.formatUrl(Api.dandanAPIInfoByBgmBangumiId, [bgmBangumiID]); var endPoint = Api.dandanAPIDomain + path; final res = await Request().get(endPoint, extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'}); Map jsonData = res.data; DanmakuEpisodeResponse danmakuEpisodeResponse = DanmakuEpisodeResponse.fromJson(jsonData); return danmakuEpisodeResponse.bangumiId; } // 从标题获取DanDanBangumiID static Future getBangumiIDByTitle(String title) async { DanmakuSearchResponse danmakuSearchResponse = await getDanmakuSearchResponse(title); int bestAnimeId = 0; double maxSimilarity = 0; for (var anime in danmakuSearchResponse.animes) { int animeId = anime.animeId; if (animeId >= 100000 || animeId < 2) { continue; } String animeTitle = anime.animeTitle; double similarity = calculateSimilarity(animeTitle, title); if (similarity == 1) { KazumiLogger().i('Danmaku: total match $title'); return animeId; } if (similarity > maxSimilarity) { maxSimilarity = similarity; bestAnimeId = animeId; KazumiLogger().i('Danmaku: match anime danmaku $title --- $animeTitle similarity: $similarity'); } } return bestAnimeId; } // 从BangumiID获取分集ID static Future getDanmakuEpisodesByBangumiID( int bangumiID) async { var path = Api.formatUrl(Api.dandanAPIInfoByBgmBangumiId, [bangumiID]); var endPoint = Api.dandanAPIDomain + path; final res = await Request().get(endPoint, extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'}); Map jsonData = res.data; DanmakuEpisodeResponse danmakuEpisodeResponse = DanmakuEpisodeResponse.fromJson(jsonData); return danmakuEpisodeResponse; } // 从DanDanBangumiID获取分集ID static Future getDanDanEpisodesByDanDanBangumiID( int bangumiID) async { var path = Api.dandanAPIInfo + bangumiID.toString(); var endPoint = Api.dandanAPIDomain + path; final res = await Request().get(endPoint, extra: {'customError': '弹幕检索错误: 获取弹幕分集ID失败'}); Map jsonData = res.data; DanmakuEpisodeResponse danmakuEpisodeResponse = DanmakuEpisodeResponse.fromJson(jsonData); return danmakuEpisodeResponse; } // 从标题检索DanDan番剧数据库 static Future getDanmakuSearchResponse( String title) async { var path = Api.dandanAPISearch; var endPoint = Api.dandanAPIDomain + path; Map keywordMap = { 'keyword': title, }; final res = await Request().get(endPoint, data: keywordMap, extra: {'customError': '弹幕检索错误: 获取弹幕番剧ID失败'}); Map jsonData = res.data; DanmakuSearchResponse danmakuSearchResponse = DanmakuSearchResponse.fromJson(jsonData); return danmakuSearchResponse; } static Future> getDanDanmaku(int bangumiID, int episode) async { List danmakus = []; if (bangumiID == 0) { return danmakus; } // 这里猜测了弹弹Play的分集命名规则,例如上面的番剧ID为1758,第一集弹幕库ID大概率为17580001,但是此命名规则并没有体现在官方API文档里,保险的做法是请求 Api.dandanInfo var path = Api.dandanAPIComment + bangumiID.toString() + episode.toString().padLeft(4, '0'); var endPoint = Api.dandanAPIDomain + path; Map withRelated = { 'withRelated': 'true', }; KazumiLogger().i("Danmaku: final request URL $endPoint"); final res = await Request().get(endPoint, data: withRelated, extra: {'customError': '弹幕检索错误: 获取弹幕失败'}); Map jsonData = res.data; List comments = jsonData['comments']; for (var comment in comments) { Danmaku danmaku = Danmaku.fromJson(comment); danmakus.add(danmaku); } return danmakus; } static Future> getDanDanmakuByEpisodeID(int episodeID) async { var path = Api.dandanAPIComment + episodeID.toString(); var endPoint = Api.dandanAPIDomain + path; List danmakus = []; Map withRelated = { 'withRelated': 'true', }; final res = await Request().get(endPoint, data: withRelated, extra: {'customError': '弹幕检索错误: 获取弹幕失败'}); Map jsonData = res.data; List comments = jsonData['comments']; for (var comment in comments) { Danmaku danmaku = Danmaku.fromJson(comment); danmakus.add(danmaku); } return danmakus; } } ================================================ FILE: lib/request/interceptor.dart ================================================ import 'package:dio/dio.dart'; import 'package:kazumi/request/api.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/mortis.dart'; import 'package:kazumi/utils/constants.dart'; class ApiInterceptor extends Interceptor { static Box setting = GStorage.setting; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // Github mirror if (options.path.contains('github')) { bool enableGitProxy = setting.get(SettingBoxKey.enableGitProxy, defaultValue: false); if (enableGitProxy) { options.path = Api.gitMirror + options.path; } } if (options.path.contains(Api.dandanAPIDomain)) { var timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; options.headers = { 'user-agent': Utils.getRandomUA(), 'referer': '', 'X-Auth': 1, 'X-AppId': mortis['id'], 'X-Timestamp': timestamp, 'X-Signature': Utils.generateDandanSignature( Uri.parse(options.path).path, timestamp), }; } if (options.path.contains(Api.bangumiAPIDomain) || options.path.contains(Api.bangumiAPINextDomain)) { options.headers = bangumiHTTPHeader; } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { String url = err.requestOptions.uri.toString(); if (!url.contains('heartBeat') && err.requestOptions.extra['customError'] != '') { if (err.requestOptions.extra['customError'] == null) { KazumiDialog.showToast( message: await dioError(err), ); } else { KazumiDialog.showToast( message: err.requestOptions.extra['customError'], ); } } super.onError(err, handler); } static Future dioError(DioException error) async { bool proxyEnable = await setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (proxyEnable) { return '代理连接异常,请检查代理设置'; } switch (error.type) { case DioExceptionType.badCertificate: return '证书有误!'; case DioExceptionType.badResponse: return '服务器异常,请稍后重试!'; case DioExceptionType.cancel: return '请求已被取消,请重新请求'; case DioExceptionType.connectionError: return '连接错误,请检查网络设置'; case DioExceptionType.connectionTimeout: return '网络连接超时,请检查网络设置'; case DioExceptionType.receiveTimeout: return '响应超时,请稍后重试!'; case DioExceptionType.sendTimeout: return '发送请求超时,请检查网络设置'; case DioExceptionType.unknown: final String res = await checkConnect(); return '$res 网络异常'; } } static Future checkConnect() async { final connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult.contains(ConnectivityResult.mobile)) { return '正在使用移动流量'; } if (connectivityResult.contains(ConnectivityResult.wifi)) { return '正在使用wifi'; } if (connectivityResult.contains(ConnectivityResult.ethernet)) { return '正在使用局域网'; } if (connectivityResult.contains(ConnectivityResult.vpn)) { return '正在使用代理网络'; } if (connectivityResult.contains(ConnectivityResult.other)) { return '正在使用其他网络'; } if (connectivityResult.contains(ConnectivityResult.none)) { return '未连接到任何网络'; } return ''; } } ================================================ FILE: lib/request/plugin.dart ================================================ import 'dart:convert'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/request/request.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:kazumi/modules/plugin/plugin_http_module.dart'; class PluginHTTP { static Future> getPluginList() async { List pluginHTTPItemList = []; try { var res = await Request().get('${Api.pluginShop}index.json'); final jsonData = json.decode(res.data); for (dynamic pluginJsonItem in jsonData) { try { PluginHTTPItem pluginHTTPItem = PluginHTTPItem.fromJson(pluginJsonItem); pluginHTTPItemList.add(pluginHTTPItem); } catch (_) {} } } catch (e) { KazumiLogger().e('Plugin: getPluginList error: ${e.toString()}'); } return pluginHTTPItemList; } static Future getPlugin(String name) async { Plugin? plugin; try { var res = await Request().get('${Api.pluginShop}$name.json'); final jsonData = json.decode(res.data); plugin = Plugin.fromJson(jsonData); } catch(e) { KazumiLogger().e('Plugin: getPlugin error: ${e.toString()}'); } return plugin; } } ================================================ FILE: lib/request/query_manager.dart ================================================ import 'dart:async'; import 'package:kazumi/modules/search/plugin_search_module.dart'; import 'package:kazumi/plugins/plugins.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/info/info_controller.dart'; import 'package:kazumi/plugins/plugins_controller.dart'; import 'package:kazumi/utils/logger.dart'; class QueryManager { QueryManager({ required this.infoController, }); final InfoController infoController; final PluginsController pluginsController = Modular.get(); StreamController? _controller; bool _isCancelled = false; Future querySource(String keyword, String pluginName) async { for (PluginSearchResponse pluginSearchResponse in infoController.pluginSearchResponseList) { if (pluginSearchResponse.pluginName == pluginName) { infoController.pluginSearchResponseList.remove(pluginSearchResponse); break; } } if (infoController.pluginSearchStatus.containsKey(pluginName)) { infoController.pluginSearchStatus[pluginName] = 'pending'; } for (Plugin plugin in pluginsController.pluginList) { if (plugin.name == pluginName) { plugin.queryBangumi(keyword, shouldRethrow: true).then((result) { if (_isCancelled) { return; } infoController.pluginSearchStatus[plugin.name] = 'success'; if (result.data.isNotEmpty) { pluginsController.validityTracker.markSearchValid(plugin.name); } infoController.pluginSearchResponseList.add(result); }).catchError((error) { if (_isCancelled) { return; } if (error is CaptchaRequiredException) { KazumiLogger().w('QueryManager: captcha required for ${error.pluginName}'); infoController.pluginSearchStatus[error.pluginName] = 'captcha'; } else if (error is NoResultException) { KazumiLogger().i('QueryManager: no results for ${error.pluginName}'); infoController.pluginSearchStatus[error.pluginName] = 'noResult'; } else { final name = error is SearchErrorException ? error.pluginName : plugin.name; KazumiLogger().w('QueryManager: search error for $name'); infoController.pluginSearchStatus[name] = 'error'; } }); } } } Future queryAllSource(String keyword) async { _controller = StreamController(); infoController.pluginSearchResponseList.clear(); for (Plugin plugin in pluginsController.pluginList) { infoController.pluginSearchStatus[plugin.name] = 'pending'; } for (Plugin plugin in pluginsController.pluginList) { if (_isCancelled) return; plugin.queryBangumi(keyword, shouldRethrow: true).then((result) { if (_isCancelled) { return; } infoController.pluginSearchStatus[plugin.name] = 'success'; if (result.data.isNotEmpty) { pluginsController.validityTracker.markSearchValid(plugin.name); } _controller?.add(result); }).catchError((error) { if (_isCancelled) { return; } if (error is CaptchaRequiredException) { KazumiLogger().w('QueryManager: captcha required for ${error.pluginName}'); infoController.pluginSearchStatus[error.pluginName] = 'captcha'; } else if (error is NoResultException) { KazumiLogger().i('QueryManager: no results for ${error.pluginName}'); infoController.pluginSearchStatus[error.pluginName] = 'noResult'; } else { final name = error is SearchErrorException ? error.pluginName : plugin.name; KazumiLogger().w('QueryManager: search error for $name'); infoController.pluginSearchStatus[name] = 'error'; } }); } await for (var result in _controller!.stream) { if (_isCancelled) break; infoController.pluginSearchResponseList.add(result); } } void cancel() { _isCancelled = true; if (_controller != null && !_controller!.isClosed) { _controller!.close(); } } } ================================================ FILE: lib/request/request.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:kazumi/request/interceptor.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:hive_ce/hive.dart'; class Request { static final Request _instance = Request._internal(); static late final Dio dio; static Box setting = GStorage.setting; factory Request() => _instance; // 初始化 (一般只在应用启动时调用) static Future setCookie() async { setOptionsHeaders(); // 初始化时检查并设置代理 final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (proxyEnable) { setProxy(); } } // 设置请求头 static void setOptionsHeaders() { dio.options.headers['referer'] = ''; dio.options.headers['user-agent'] = Utils.getRandomUA(); } // 设置代理(仅支持 HTTP 代理) static void setProxy() { final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) { disableProxy(); return; } final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final parsed = ProxyUtils.parseProxyUrl(proxyUrl); if (parsed == null) { KazumiLogger().w('Proxy: 代理地址格式错误或为空'); return; } final (proxyHost, proxyPort) = parsed; dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final HttpClient client = HttpClient(); client.findProxy = (Uri uri) { return 'PROXY $proxyHost:$proxyPort'; }; // 忽略证书验证 client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; return client; }, ); KazumiLogger().i('Proxy: HTTP 代理设置成功 $proxyHost:$proxyPort'); } // 禁用代理 static void disableProxy() { dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final HttpClient client = HttpClient(); return client; }, ); KazumiLogger().i('Proxy: 代理已禁用'); } Request._internal() { //BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数 BaseOptions options = BaseOptions( //请求基地址,可以包含子路径 baseUrl: '', //连接服务器超时时间,单位是毫秒. connectTimeout: const Duration(milliseconds: 12000), //响应流上前后两次接受到数据的间隔,单位为毫秒。 receiveTimeout: const Duration(milliseconds: 12000), //Http请求头. headers: {}, ); // enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy, // defaultValue: false) as bool; dio = Dio(options); // debugPrint('Dio 初始化完成'); // if (enableSystemProxy) { // setProxy(); // debugPrint('系统代理启用'); // } // 拦截器 dio.interceptors.add(ApiInterceptor()); // 日志拦截器 输出请求、响应内容 dio.interceptors.add(LogInterceptor( request: false, requestHeader: false, responseHeader: false, )); dio.transformer = BackgroundTransformer(); dio.options.validateStatus = (int? status) { return status! >= 200 && status < 300; }; } Future get(url, {data, options, cancelToken, extra, bool shouldRethrow = false}) async { Response response; ResponseType resType = ResponseType.json; options ??= Options(); if (extra != null) { resType = extra!['resType'] ?? ResponseType.json; if (extra['ua'] != null) { options.headers = {'user-agent': headerUa(type: extra['ua'])}; } if (extra['customError'] != null) { options.extra = {'customError': extra['customError']}; } } options.responseType = resType; try { response = await dio.get( url, queryParameters: data, options: options, cancelToken: cancelToken, ); return response; } on DioException catch (e) { if (shouldRethrow) { rethrow; } Response errResponse = Response( data: { 'message': await ApiInterceptor.dioError(e) }, // 将自定义 Map 数据赋值给 Response 的 data 属性 statusCode: 200, requestOptions: RequestOptions(), ); return errResponse; } } Future post(url, {data, queryParameters, options, cancelToken, extra, bool shouldRethrow = false}) async { // print('post-data: $data'); Response response; try { response = await dio.post( url, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); // print('post success: ${response.data}'); return response; } on DioException catch (e) { if (shouldRethrow) { rethrow; } Response errResponse = Response( data: { 'message': await ApiInterceptor.dioError(e) }, // 将自定义 Map 数据赋值给 Response 的 data 属性 statusCode: 200, requestOptions: RequestOptions(), ); return errResponse; } } String headerUa({type = 'mob'}) { return Utils.getRandomUA(); } } ================================================ FILE: lib/shaders/shaders_controller.dart ================================================ import 'dart:io'; import 'package:mobx/mobx.dart'; import 'package:flutter/services.dart' show rootBundle, AssetManifest; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:kazumi/utils/logger.dart'; part 'shaders_controller.g.dart'; class ShadersController = _ShadersController with _$ShadersController; abstract class _ShadersController with Store { late Directory shadersDirectory; Future copyShadersToExternalDirectory() async { final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle); final assets = assetManifest.listAssets(); final directory = await getApplicationSupportDirectory(); shadersDirectory = Directory(path.join(directory.path, 'anime_shaders')); if (!await shadersDirectory.exists()) { await shadersDirectory.create(recursive: true); KazumiLogger() .i('ShaderManager: Create GLSL Shader: ${shadersDirectory.path}'); } final shaderFiles = assets.where((String asset) => asset.startsWith('assets/shaders/') && asset.endsWith('.glsl')); int copiedFilesCount = 0; for (var filePath in shaderFiles) { final fileName = filePath.split('/').last; final targetFile = File(path.join(shadersDirectory.path, fileName)); if (await targetFile.exists()) { KazumiLogger() .i('ShaderManager: GLSL Shader exists, skip: ${targetFile.path}'); continue; } try { final data = await rootBundle.load(filePath); final List bytes = data.buffer.asUint8List(); await targetFile.writeAsBytes(bytes); copiedFilesCount++; KazumiLogger().i('ShaderManager: Copy: ${targetFile.path}'); } catch (e) { KazumiLogger().e('ShaderManager: Copy: ($filePath)', error: e); } } KazumiLogger().i( 'ShaderManager: $copiedFilesCount GLSL files copied to ${shadersDirectory.path}'); } } ================================================ FILE: lib/shaders/shaders_controller.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'shaders_controller.dart'; // ************************************************************************** // StoreGenerator // ************************************************************************** // 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 mixin _$ShadersController on _ShadersController, Store { @override String toString() { return ''' '''; } } ================================================ FILE: lib/utils/anime_season.dart ================================================ /// This class asks for DateTime to get a string to indicate seasonal anime class AnimeSeason { late DateTime _date; final _seasons = ['冬季', '春季', '夏季', '秋季']; AnimeSeason(DateTime date) { _date = date; } List _getYearAndSeason(DateTime dt) { int year = dt.year; int month = dt.month; int season; if ((month == 1) || (month == 2) || (month == 3)) { season = 0; } else if ((month == 4) || (month == 5) || (month == 6)) { season = 1; } else if ((month == 7) || (month == 8) || (month == 9)) { season = 2; } else { season = 3; } return [year, season]; } // 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'] // 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 List toSeasonStartAndEnd() { var yas = _getYearAndSeason(_date); int year = yas[0]; int season = yas[1]; var end = DateTime(year, (season + 1) * 3, 1); int startMonth = season * 3; if (startMonth == 0) { startMonth = 12; year--; } var start = DateTime(year, startMonth, 1); return [start.toString(), end.toString()]; } @override String toString() { var yas = _getYearAndSeason(_date); return '${yas[0]}年${_seasons[yas[1]]}新番'; } } ================================================ FILE: lib/utils/auto_updater.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:open_filex/open_filex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; /// 安装类型枚举 enum InstallationType { windowsMsix, // Kazumi_windows_1.7.5.msix windowsPortable, // Kazumi_windows_1.7.5.zip linuxDeb, // Kazumi_linux_1.7.5_amd64.deb linuxTar, // Kazumi_linux_1.7.5_amd64.tar.gz macosDmg, // Kazumi_macos_1.7.5.dmg androidApk, // Kazumi_android_1.7.5.apk ios, // iOS App unknown, } /// 更新信息类 class UpdateInfo { final String version; final String description; final String downloadUrl; final String releaseNotes; final String publishedAt; final InstallationType? installationType; final List availableInstallationTypes; final List assets; UpdateInfo({ required this.version, required this.description, required this.downloadUrl, required this.releaseNotes, required this.publishedAt, this.installationType, this.availableInstallationTypes = const [], this.assets = const [], }); /// 获取默认的安装类型(第一个可用类型) InstallationType get recommendedInstallationType { if (availableInstallationTypes.isNotEmpty) { return availableInstallationTypes.first; } return installationType ?? InstallationType.unknown; } } class AutoUpdater { static final AutoUpdater _instance = AutoUpdater._internal(); factory AutoUpdater() => _instance; AutoUpdater._internal(); final Dio _dio = Dio(); Box get setting => GStorage.setting; /// 检测所有可能的安装类型 Future> _detectAvailableInstallationTypes() async { List availableTypes = []; try { if (Platform.isWindows) { // Windows 平台支持 MSIX 和 ZIP 便携版 availableTypes.add(InstallationType.windowsMsix); availableTypes.add(InstallationType.windowsPortable); } else if (Platform.isLinux) { // Linux 平台支持 DEB 和 TAR.GZ availableTypes.add(InstallationType.linuxDeb); availableTypes.add(InstallationType.linuxTar); } else if (Platform.isMacOS) { // macOS 平台支持 DMG availableTypes.add(InstallationType.macosDmg); } else if (Platform.isIOS) { // iOS 平台通过 Github availableTypes.add(InstallationType.ios); } else if (Platform.isAndroid) { // Android 平台支持 APK availableTypes.add(InstallationType.androidApk); } } catch (e) { KazumiLogger().w('Update: detect installation types failed', error: e); } if (availableTypes.isEmpty) { availableTypes.add(InstallationType.unknown); } return availableTypes; } /// 检查是否有新版本可用 Future checkForUpdates() async { try { final response = await _dio.get(Api.latestApp); final data = response.data; if (data == null || !data.containsKey('tag_name')) { throw Exception('无效的响应数据'); } final remoteVersion = data['tag_name'] as String; final currentVersion = Api.version; if (Utils.needUpdate(currentVersion, remoteVersion)) { final availableTypes = await _detectAvailableInstallationTypes(); return UpdateInfo( version: remoteVersion, description: data['body'] ?? '发现新版本', downloadUrl: '', // 将在用户选择安装类型后填充 releaseNotes: data['html_url'] ?? '', publishedAt: data['published_at'] ?? '', installationType: availableTypes.first, // 保持兼容性 availableInstallationTypes: availableTypes, assets: data['assets'] ?? [], ); } return null; } catch (e) { KazumiLogger().e('Update: check for updates failed', error: e); rethrow; } } /// 自动检查更新(仅在启用自动更新时) Future autoCheckForUpdates() async { final autoUpdate = setting.get(SettingBoxKey.autoUpdate, defaultValue: true); if (!autoUpdate) return; try { final updateInfo = await checkForUpdates(); if (updateInfo != null) { _showUpdateDialog(updateInfo, isAutoCheck: true); } } catch (e) { // 自动检查失败时不显示错误 KazumiLogger().w('Update: auto check for updates failed', error: e); } } /// 手动检查更新 Future manualCheckForUpdates() async { try { final updateInfo = await checkForUpdates(); if (updateInfo != null) { _showUpdateDialog(updateInfo, isAutoCheck: false); } else { KazumiDialog.showToast(message: '当前已经是最新版本!'); } } catch (e) { KazumiDialog.showToast(message: '检查更新失败'); } } /// 显示更新对话框 void _showUpdateDialog(UpdateInfo updateInfo, {bool isAutoCheck = false}) { KazumiDialog.show( builder: (context) { return AlertDialog( title: Text('发现新版本 ${updateInfo.version}'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(updateInfo.description), if (updateInfo.publishedAt.isNotEmpty) ...[ const SizedBox(height: 8), Text( '发布时间: ${Utils.formatDate(updateInfo.publishedAt)}', style: Theme.of(context).textTheme.bodySmall, ), ], const SizedBox(height: 8), if (!Platform.isLinux && !Platform.isIOS) ...[ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择安装类型:', style: Theme.of(context).textTheme.labelSmall, ), const SizedBox(height: 8), ...updateInfo.availableInstallationTypes.map((type) { return Container( margin: const EdgeInsets.symmetric(vertical: 2), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () { KazumiDialog.dismiss(); _downloadUpdateWithType(updateInfo, type); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), decoration: BoxDecoration( border: Border.all( color: Theme.of(context) .colorScheme .outline .withValues(alpha: 0.3), ), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Icon( Icons.download, size: 16, color: Theme.of(context) .colorScheme .primary, ), const SizedBox(width: 8), Expanded( child: Text( _getInstallationTypeDescription(type), style: Theme.of(context) .textTheme .bodySmall, ), ), Icon( Icons.arrow_forward_ios, size: 12, color: Theme.of(context) .colorScheme .outline, ), ], ), ), ), ), ); }), ], ), ), ], ], ), ), actions: [ if (isAutoCheck) TextButton( onPressed: () { setting.put(SettingBoxKey.autoUpdate, false); KazumiDialog.dismiss(); KazumiDialog.showToast(message: '已关闭自动更新'); }, child: Text( '关闭自动更新', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '稍后提醒', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), if (updateInfo.releaseNotes.isNotEmpty) TextButton( onPressed: () { launchUrl(Uri.parse(updateInfo.releaseNotes), mode: LaunchMode.externalApplication); }, child: const Text('查看详情'), ), TextButton( onPressed: () { KazumiDialog.dismiss(); // 直接使用第一个可用的安装类型 if (updateInfo.availableInstallationTypes.isNotEmpty) { _downloadUpdateWithType( updateInfo, updateInfo.availableInstallationTypes.first); } }, child: const Text('立即更新'), ), ], ); }, ); } /// 获取安装类型的描述 String _getInstallationTypeDescription(InstallationType type) { switch (type) { case InstallationType.windowsMsix: return 'Windows MSIX 包'; case InstallationType.windowsPortable: return 'Windows 便携版 (ZIP)'; case InstallationType.linuxDeb: return 'Linux DEB 包'; case InstallationType.linuxTar: return 'Linux TAR 包'; case InstallationType.macosDmg: return 'macOS DMG 镜像'; case InstallationType.androidApk: return 'Android APK'; case InstallationType.ios: return 'iOS ipa'; case InstallationType.unknown: return '未知安装类型'; } } /// 根据选择的类型下载更新 Future _downloadUpdateWithType( UpdateInfo updateInfo, InstallationType selectedType) async { try { // iOS 和 Linux 直接跳转到 Release 页面 if (selectedType == InstallationType.ios || selectedType == InstallationType.linuxDeb || selectedType == InstallationType.linuxTar) { String releaseUrl = updateInfo.releaseNotes; if (releaseUrl.isEmpty) { releaseUrl = Api.latestApp; } launchUrl(Uri.parse(releaseUrl), mode: LaunchMode.externalApplication); return; } final downloadUrl = await _getDownloadUrlForType(updateInfo.assets, selectedType); if (downloadUrl.isEmpty) { KazumiDialog.showToast( message: '没有找到 ${_getInstallationTypeDescription(selectedType)} 的下载链接'); return; } // 获取文件的 SHA256 哈希值用于验证 final expectedHash = _getFileHashFromAssets(updateInfo.assets, downloadUrl); // 创建一个临时的 UpdateInfo 对象用于下载 final downloadInfo = UpdateInfo( version: updateInfo.version, description: updateInfo.description, downloadUrl: downloadUrl, releaseNotes: updateInfo.releaseNotes, publishedAt: updateInfo.publishedAt, installationType: selectedType, availableInstallationTypes: [selectedType], assets: updateInfo.assets, ); _downloadUpdate(downloadInfo, expectedHash); } catch (e) { KazumiDialog.showToast(message: '下载失败: ${e.toString()}'); KazumiLogger().e('Update: download update failed', error: e); } } /// 下载更新 Future _downloadUpdate( UpdateInfo updateInfo, String expectedHash) async { if (updateInfo.downloadUrl.isEmpty) { KazumiDialog.showToast(message: '没有找到合适的下载链接'); return; } // 显示下载进度对话框 KazumiDialog.show( clickMaskDismiss: false, builder: (context) { return AlertDialog( title: const Text('正在下载更新'), content: Column( mainAxisSize: MainAxisSize.min, children: [ ValueListenableBuilder( valueListenable: _downloadProgress, builder: (context, value, child) { return Column( children: [ LinearProgressIndicator(value: value), const SizedBox(height: 8), Text('${(value * 100).toStringAsFixed(1)}%'), ], ); }, ), ], ), actions: [ TextButton( onPressed: () { _cancelDownload(); KazumiDialog.dismiss(); }, child: const Text('取消'), ), ], ); }, ); try { final downloadPath = await _downloadFile( updateInfo.downloadUrl, updateInfo.version, expectedHash); // 不自动关闭对话框,而是显示下载完成状态 _showDownloadCompleteDialog(downloadPath, updateInfo); } catch (e) { KazumiDialog.dismiss(); // 显示详细的错误信息 String errorMessage = '下载失败'; if (e.toString().contains('Permission denied') || e.toString().contains('Operation not permitted')) { errorMessage = '权限不足,文件已保存到应用临时目录'; } else if (e.toString().contains('No space left')) { errorMessage = '磁盘空间不足'; } else if (e.toString().contains('Network')) { errorMessage = '网络连接错误'; } else if (e.toString().contains('文件完整性验证失败')) { errorMessage = '文件完整性验证失败,可能是网络传输错误'; } KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('下载失败'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(errorMessage), const SizedBox(height: 8), Text( '错误详情: ${e.toString()}', style: Theme.of(context).textTheme.bodySmall, ), ], ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: const Text('确定'), ), TextButton( onPressed: () { KazumiDialog.dismiss(); // 重新尝试下载 _downloadUpdate(updateInfo, expectedHash); }, child: const Text('重试'), ), ], ); }, ); KazumiLogger().e('Update: download update failed', error: e); } } final ValueNotifier _downloadProgress = ValueNotifier(0.0); CancelToken? _cancelToken; void _cancelDownload() { _cancelToken?.cancel(); } /// 显示下载完成对话框 void _showDownloadCompleteDialog(String filePath, UpdateInfo updateInfo) { // 替换当前的下载进度对话框内容 KazumiDialog.dismiss(); KazumiDialog.show( builder: (context) { return AlertDialog( title: const Text('下载完成'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.check_circle, color: Theme.of(context).colorScheme.primary, size: 20, ), const SizedBox(width: 8), Expanded( child: Text('新版本 ${updateInfo.version} 已下载完成'), ), ], ), const SizedBox(height: 12), Text( '安装过程中应用将会退出', style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '文件位置:', style: Theme.of(context).textTheme.labelSmall, ), const SizedBox(height: 4), SelectableText( filePath, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '稍后安装', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), if (Utils.isDesktop()) TextButton( onPressed: () { // 在文件管理器中显示文件 _revealInFileManager(filePath); }, child: const Text('打开文件夹'), ), TextButton( onPressed: () { KazumiDialog.dismiss(); _installUpdate( filePath, updateInfo.recommendedInstallationType); }, child: const Text('立即安装'), ), ], ); }, ); } /// 下载文件 Future _downloadFile( String url, String version, String expectedHash) async { final fileName = _getFileNameFromUrl(url, version); // 统一使用临时目录 final tempDir = await getTemporaryDirectory(); final filePath = '${tempDir.path}/$fileName'; final file = File(filePath); // 检查文件是否已存在 if (await file.exists()) { try { //使用哈希验证文件完整性 final localHash = await Utils.calculateFileHash(file); if (localHash == expectedHash) { // 文件已存在且哈希匹配,直接返回 KazumiLogger().i('Update: file already exists and hash verified, skipping download: $filePath'); _downloadProgress.value = 1.0; return filePath; } else { // 文件存在但哈希不匹配,删除后重新下载 KazumiLogger().i( 'Update: file hash mismatch detected (local: $localHash, expected: $expectedHash), deleting and re-downloading'); await file.delete(); } } catch (e) { // 验证过程中出错,删除文件重新下载 KazumiLogger().w('Update: file verification failed, deleting and re-downloading', error: e); if (await file.exists()) { await file.delete(); } } } _cancelToken = CancelToken(); await _dio.download( url, filePath, cancelToken: _cancelToken, onReceiveProgress: (received, total) { if (total > 0) { _downloadProgress.value = received / total; } }, ); // 下载完成后验证文件哈希 final downloadedHash = await Utils.calculateFileHash(file); if (downloadedHash != expectedHash) { // 哈希不匹配,删除文件并抛出异常 await file.delete(); throw Exception('文件完整性验证失败: 期望 $expectedHash,实际 $downloadedHash'); } KazumiLogger().i('Update: file downloaded and hash verified: $filePath'); return filePath; } /// 安装更新 void _installUpdate( String filePath, InstallationType installationType) async { try { // 显示准备退出的提示 KazumiDialog.showToast(message: '准备安装更新,应用即将退出...'); await Future.delayed(const Duration(seconds: 2)); if (Platform.isWindows) { if (installationType == InstallationType.windowsMsix) { final Uri fileUri = Uri.file(filePath); if (await canLaunchUrl(fileUri)) { await launchUrl(fileUri); } else { throw 'Could not launch $fileUri'; } } else { await Process.start('explorer.exe', [filePath], runInShell: true); } await Future.delayed(const Duration(seconds: 1)); exit(0); } else if (Platform.isMacOS) { if (filePath.endsWith('.dmg')) { await Process.start('open', [filePath]); exit(0); } } else if (Platform.isAndroid) { final result = await OpenFilex.open(filePath); if (result.type != ResultType.done) { KazumiDialog.showToast(message: '无法打开安装文件: ${result.message}'); return; } } } catch (e) { KazumiDialog.showToast(message: '启动安装程序失败: ${e.toString()}'); KazumiLogger().e('Update: launch installer failed', error: e); } } /// 在文件管理器中显示文件 void _revealInFileManager(String filePath) async { try { final type = await FileSystemEntity.type(filePath); String targetDirOrFile; // 如果传入的本来就是目录则打开这个目录 // 如果是文件则打开包含它的目录 if (type == FileSystemEntityType.notFound) { KazumiDialog.showToast(message: '文件或目录不存在'); return; } else if (type == FileSystemEntityType.directory) { targetDirOrFile = filePath; } else { targetDirOrFile = File(filePath).parent.path; } if (Platform.isWindows) { if (type == FileSystemEntityType.file) { final arg = '/select,${filePath.replaceAll('/', r'\')}'; await Process.start('explorer.exe', [arg], runInShell: true); } else { await Process.start('explorer.exe', [targetDirOrFile.replaceAll('/', r'\')], runInShell: true); } } else if (Platform.isMacOS) { if (type == FileSystemEntityType.file) { await Process.start('open', ['-R', filePath]); } else { await Process.start('open', [targetDirOrFile]); } } else if (Platform.isLinux) { // 尝试打开包含文件的文件夹 await Process.start('xdg-open', [targetDirOrFile]); } else { KazumiDialog.showToast(message: '此平台不支持通过此方法打开文件管理器'); } } catch (e) { KazumiDialog.showToast(message: '无法打开文件管理器'); KazumiLogger().w('Update: reveal in file manager failed', error: e); } finally { try { // 确保对话框被关闭 KazumiDialog.dismiss(); } catch (_) {} } } /// 根据安装类型获取下载链接 Future _getDownloadUrlForType( List assets, InstallationType type) async { final patterns = _getFilePatterns(type).map((p) => p.toLowerCase()).toList(); try { final asset = assets.cast>().firstWhere((asset) { final name = (asset['name'] as String?)?.toLowerCase() ?? ''; final downloadUrl = (asset['browser_download_url'] as String?) ?? ''; return downloadUrl.isNotEmpty && patterns.every((pattern) => name.contains(pattern)); }); return (asset['browser_download_url'] as String?) ?? ''; } catch (e) { return ''; } } /// 获取合适的下载链接 /// 根据安装类型获取文件名模式 List _getFilePatterns(InstallationType installationType) { switch (installationType) { case InstallationType.windowsMsix: return ['windows', '.msix']; case InstallationType.windowsPortable: return ['windows', '.zip']; case InstallationType.macosDmg: return ['macos', '.dmg']; case InstallationType.androidApk: return ['android', '.apk']; // 以下类型直接跳转到 GitHub Release 页面,不需要下载文件 case InstallationType.linuxDeb: case InstallationType.linuxTar: case InstallationType.ios: case InstallationType.unknown: return []; } } /// 从URL获取文件名 String _getFileNameFromUrl(String url, String version) { final uri = Uri.parse(url); final fileName = uri.pathSegments.last; if (fileName.isNotEmpty) { return fileName; } // 回退方案 String extension = ''; if (Platform.isWindows) { extension = '.msix'; } else if (Platform.isMacOS) { extension = '.dmg'; } else if (Platform.isLinux) { extension = '.deb'; } else if (Platform.isAndroid) { extension = '.apk'; } return 'Kazumi-$version$extension'; } /// 从 assets 中获取文件的哈希值 String _getFileHashFromAssets(List assets, String downloadUrl) { for (final asset in assets) { final assetDownloadUrl = asset['browser_download_url'] as String? ?? ''; if (assetDownloadUrl == downloadUrl) { final digest = asset['digest'] as String? ?? ''; if (digest.isNotEmpty && digest.startsWith('sha256:')) { return digest.substring(7); // 移除 "sha256:" 前缀 } } } return ''; } } ================================================ FILE: lib/utils/background_download_service.dart ================================================ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:kazumi/utils/logger.dart'; /// Android 后台下载服务 /// /// 使用 Foreground Service 保持 app 进程存活,防止系统在后台时杀死下载进程。 /// 下载逻辑仍在主 Isolate 运行,此服务仅负责: /// 1. 显示通知栏进度 /// 2. 保持进程存活 /// 3. 提供通知栏交互(暂停/取消) class BackgroundDownloadService { static final BackgroundDownloadService _instance = BackgroundDownloadService._internal(); factory BackgroundDownloadService() => _instance; BackgroundDownloadService._internal(); bool _isInitialized = false; bool _isRunning = false; void Function()? onPauseAll; void Function()? onCancelAll; void Function()? onNavigateToDownloadRequested; /// 返回 true 表示用户同意请求权限,false 表示用户拒绝 Future Function()? onNotificationPermissionRequired; bool get isSupported => Platform.isAndroid; bool get isRunning => _isRunning; Future init() async { if (!isSupported || _isInitialized) return; FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'kazumi_download_channel', channelName: '下载服务', channelDescription: '视频下载后台服务', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, onlyAlertOnce: true, ), iosNotificationOptions: const IOSNotificationOptions( showNotification: false, ), foregroundTaskOptions: ForegroundTaskOptions( eventAction: ForegroundTaskEventAction.nothing(), autoRunOnBoot: false, autoRunOnMyPackageReplaced: false, allowWakeLock: true, allowWifiLock: true, ), ); FlutterForegroundTask.initCommunicationPort(); _isInitialized = true; KazumiLogger().i('BackgroundDownloadService: initialized'); } Future needsNotificationPermission() async { if (!isSupported) return false; final permission = await FlutterForegroundTask.checkNotificationPermission(); return permission != NotificationPermission.granted; } Future requestNotificationPermission() async { if (!isSupported) return true; final result = await FlutterForegroundTask.requestNotificationPermission(); return result == NotificationPermission.granted; } Future startService() async { if (!isSupported) return false; if (_isRunning) return true; if (!_isInitialized) { await init(); } final needsPermission = await needsNotificationPermission(); if (needsPermission) { if (onNotificationPermissionRequired != null) { final userAgreed = await onNotificationPermissionRequired!(); if (userAgreed) { final granted = await requestNotificationPermission(); if (!granted) { KazumiLogger().w('BackgroundDownloadService: notification permission denied by user'); } } else { KazumiLogger().i('BackgroundDownloadService: user declined permission dialog'); } } else { // 没有设置回调,直接请求权限(兼容旧行为) final granted = await requestNotificationPermission(); if (!granted) { KazumiLogger().w('BackgroundDownloadService: notification permission denied'); } } } try { final result = await FlutterForegroundTask.startService( notificationTitle: '正在下载', notificationText: '准备中...', notificationButtons: [ const NotificationButton(id: 'pause_all', text: '暂停全部'), ], callback: _backgroundCallback, ); _isRunning = result is ServiceRequestSuccess; if (_isRunning) { KazumiLogger().i('BackgroundDownloadService: service started'); } else { KazumiLogger().w('BackgroundDownloadService: service start returned non-success: $result'); } return _isRunning; } catch (e) { KazumiLogger().e('BackgroundDownloadService: failed to start service', error: e); return false; } } Future stopService() async { if (!isSupported || !_isRunning) return; try { await FlutterForegroundTask.stopService(); _isRunning = false; KazumiLogger().i('BackgroundDownloadService: service stopped'); } catch (e) { KazumiLogger().e('BackgroundDownloadService: failed to stop service', error: e); } } Future updateNotification({ required String title, required String text, }) async { if (!isSupported || !_isRunning) return; try { await FlutterForegroundTask.updateService( notificationTitle: title, notificationText: text, ); } catch (e) { // 忽略更新失败,不影响下载 } } Future updateProgress({ required int activeCount, required int totalCount, required double overallProgress, required String speedText, }) async { if (!isSupported || !_isRunning) return; String title; String text; if (activeCount == 0) { title = '下载已暂停'; text = '共 $totalCount 个任务'; } else { final percent = (overallProgress * 100).toInt(); title = '正在下载 ($activeCount/$totalCount)'; text = '$percent% · $speedText'; } await updateNotification(title: title, text: text); } Future showCompletedNotification({ required int completedCount, }) async { if (!isSupported) return; await stopService(); // TODO: 显示普通通知告知用户下载完成(需要额外的通知插件) } void handleNotificationAction(String buttonId) { switch (buttonId) { case 'pause_all': onPauseAll?.call(); break; case 'cancel_all': onCancelAll?.call(); break; } } void handleNavigateToDownload() { onNavigateToDownloadRequested?.call(); } void addTaskDataCallback(void Function(Object) callback) { FlutterForegroundTask.addTaskDataCallback(callback); } void removeTaskDataCallback(void Function(Object) callback) { FlutterForegroundTask.removeTaskDataCallback(callback); } } /// 后台任务回调(在独立 Isolate 中运行) /// /// 注意:此回调主要用于保持服务存活和处理通知交互。 /// 实际下载逻辑在主 Isolate 中运行。 @pragma('vm:entry-point') void _backgroundCallback() { FlutterForegroundTask.setTaskHandler(_DownloadTaskHandler()); } class _DownloadTaskHandler extends TaskHandler { @override Future onStart(DateTime timestamp, TaskStarter starter) async { debugPrint('BackgroundDownloadService: task handler started'); } @override void onRepeatEvent(DateTime timestamp) { // eventAction 配置为 nothing,不会触发 } @override void onNotificationButtonPressed(String id) { debugPrint('BackgroundDownloadService: notification button pressed: $id'); FlutterForegroundTask.sendDataToMain({'action': 'button_pressed', 'id': id}); } @override void onNotificationPressed() { FlutterForegroundTask.sendDataToMain({'action': 'navigate_to_download'}); FlutterForegroundTask.launchApp(); } @override void onNotificationDismissed() { // 前台服务通知通常不可划掉 } @override Future onDestroy(DateTime timestamp, bool isTimeout) async { debugPrint('BackgroundDownloadService: task handler destroyed (isTimeout: $isTimeout)'); } @override void onReceiveData(Object data) { debugPrint('BackgroundDownloadService: received data: $data'); } } ================================================ FILE: lib/utils/constants.dart ================================================ import 'package:flutter/material.dart'; import 'package:kazumi/request/api.dart'; class StyleString { static const double cardSpace = 8; static const double safeSpace = 12; static BorderRadius mdRadius = BorderRadius.circular(10); static const Radius imgRadius = Radius.circular(12); static const double aspectRatio = 16 / 10; } const String customAppFontFamily = "MI_Sans_Regular"; /// `year2023` flag is deprecated since 3.29 but not default to false yet. Keep /// it to false so we have the latest M3 style process indicator. /// ignore: deprecated_member_use const ProgressIndicatorThemeData progressIndicatorTheme2024 = ProgressIndicatorThemeData(year2023: false); /// `year2023` flag is deprecated since 3.29 but not default to false yet. Keep /// it to false so we have the latest M3 style slider. /// ignore: deprecated_member_use const SliderThemeData sliderTheme2024 = SliderThemeData( year2023: false, showValueIndicator: ShowValueIndicator.always, ); /// The page transition method defined here is managed by flutter, and the native transition method of flutter is set here. /// Transition method here will be overridden by the transition method of modular, and do not set the transition method in modular to prevent /// the native transition method from failing const PageTransitionsTheme pageTransitionsTheme2024 = PageTransitionsTheme( builders: { TargetPlatform.android: CupertinoPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), }, ); /// Layout breakpoint according to google: /// https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes. /// /// **It's only a suggestion since not every device meet the breakpoint requirement. /// You need to build layout with some more judgements.** /// /// Some example device(portrait) width x height: /// /// * iPhone SE3: 375 x 667 /// * iPhone 16: 393 x 852 /// * iPad Pro 11-inch: 834 x 1210 /// * HW MATE60 Pro: 387.7 x 836.9 /// * OHOS in floating window: 387.7 x 631.7 or 218.1 class LayoutBreakpoint { static const Map compact = {'width': 600, 'height': 480}; static const Map medium = {'width': 840, 'height': 900}; } /// 随机UA列表 const List userAgentsList = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', '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', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0', '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', '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', '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', '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', ]; /// 默认 SyncPlay 服务器列表 const List defaultSyncPlayEndPoints = [ 'syncplay.pl:8995', 'syncplay.pl:8996', 'syncplay.pl:8997', 'syncplay.pl:8998', 'syncplay.pl:8999', ]; const String defaultSyncPlayEndPoint = 'syncplay.pl:8996'; /// 随机HTTP请求头accept-language字段列表 const List acceptLanguageList = [ 'zh-CN,zh;q=0.9', 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6', ]; /// Bangumi API 文档要求的UA格式 Map bangumiHTTPHeader = { 'user-agent': 'Predidit/Kazumi/${Api.version} (Android) (https://github.com/Predidit/Kazumi)', 'referer': '', 'content-type': 'application/json' }; /// 可选硬件解码器 const Map hardwareDecodersList = { 'auto': '启用任意可用解码器', 'auto-safe': '启用最佳解码器', 'auto-copy': '启用带拷贝功能的最佳解码器', 'd3d11va': 'DirectX11 (windows8 及以上)', 'd3d11va-copy': 'DirectX11 (windows8 及以上) (非直通)', 'videotoolbox': 'VideoToolbox (macOS / iOS)', 'videotoolbox-copy': 'VideoToolbox (macOS / iOS) (非直通)', 'vaapi': 'VAAPI (Linux)', 'vaapi-copy': 'VAAPI (Linux) (非直通)', 'nvdec': 'NVDEC (NVIDIA独占)', 'nvdec-copy': 'NVDEC (NVIDIA独占) (非直通)', 'drm': 'DRM (Linux)', 'drm-copy': 'DRM (Linux) (非直通)', 'vulkan': 'Vulkan (全平台) (实验性)', 'vulkan-copy': 'Vulkan (全平台) (实验性) (非直通)', 'dxva2': 'DXVA2 (Windows7 及以上)', 'dxva2-copy': 'DXVA2 (Windows7 及以上) (非直通)', 'vdpau': 'VDPAU (Linux)', 'vdpau-copy': 'VDPAU (Linux) (非直通)', 'mediacodec': 'MediaCodec (Android)', 'mediacodec-copy': 'MediaCodec (Android) (非直通)', 'cuda': 'CUDA (NVIDIA独占) (过时)', 'cuda-copy': 'CUDA (NVIDIA独占) (过时) (非直通)', 'crystalhd': 'CrystalHD (全平台) (过时)', 'rkmpp': 'Rockchip MPP (仅部分Rockchip芯片)', }; /// Android 可选视频渲染器 const Map androidVideoRenderersList = { 'auto': '自动选择', 'gpu': '基于 OpenGL, 通用和稳健的选项', 'gpu-next': '基于 Vulkan, 在新设备上表现最好', 'mediacodec_embed': '功耗最低,不支持超分辨率', }; /// 超分辨率滤镜 const List mpvAnime4KShaders = [ 'Anime4K_Clamp_Highlights.glsl', 'Anime4K_Restore_CNN_VL.glsl', 'Anime4K_Upscale_CNN_x2_VL.glsl', 'Anime4K_AutoDownscalePre_x2.glsl', 'Anime4K_AutoDownscalePre_x4.glsl', 'Anime4K_Upscale_CNN_x2_M.glsl' ]; /// 超分辨率滤镜 (轻量) const List mpvAnime4KShadersLite = [ 'Anime4K_Clamp_Highlights.glsl', 'Anime4K_Restore_CNN_M.glsl', 'Anime4K_Restore_CNN_S.glsl', 'Anime4K_Upscale_CNN_x2_M.glsl', 'Anime4K_AutoDownscalePre_x2.glsl', 'Anime4K_AutoDownscalePre_x4.glsl', 'Anime4K_Upscale_CNN_x2_S.glsl' ]; /// 可选播放倍速 const List defaultPlaySpeedList = [ 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, ]; const String danmakuOnSvg = ''' '''; /// 可选默认视频比例 const Map aspectRatioTypeMap = { 1: "自动", 2: "裁切填充", 3: "拉伸填充", }; /// 可选播放器日志等级 /// LogLevel 0: 错误 1: 警告 2: 简略 3: 详细 4: 调试(隐藏) 5: 全部(隐藏) const Map playerLogLevelMap = { 0: "错误", 1: "警告", 2: "简略", 3: "详细", // 以下两个级别被MPV官方支持,但是输出内容过于冗长,暂时隐藏 // 4: "调试", // 5: "全部", }; final List defaultAnimeTags = const [ '日常', '原创', '校园', '搞笑', '奇幻', '百合', '恋爱', '悬疑', '热血', '后宫', '机战', '轻改', '偶像', '治愈', '异世界', ]; // 播放器默认快捷键 final Map> defaultShortcuts = const { 'playorpause': [' '], 'forward': ['Arrow Right'], 'rewind': ['Arrow Left'], 'next': ['N'], 'prev': ['P'], 'volumeup': ['Arrow Up'], 'volumedown': ['Arrow Down'], 'togglemute': ['M'], 'fullscreen': ['F'], 'exitfullscreen': ['Escape'], 'toggledanmaku': ['D'], 'screenshot': ['S'], 'skip': ['K'], 'speed1': ['1'], 'speed2': ['2'], 'speed3': ['3'], 'speedup': ['X'], 'speeddown': ['Z'], }; // 键位别名 final Map keyAliases = { ' ': '空格', 'Arrow Up': '↑', 'Arrow Down': '↓', 'Arrow Left': '←', 'Arrow Right': '→', 'Enter': '回车', 'Tab': 'Tab', 'Escape': 'Esc', 'Backspace': '退格', }; //功能中文名对应 final Map shortcutsChineseName = { 'playorpause': '播放 / 暂停', 'forward': '快进 / 长按倍速', 'rewind': '快退', 'next': '下一集', 'prev': '上一集', 'volumeup': '音量加', 'volumedown': '音量减', 'togglemute': '静音', 'fullscreen': '全屏', 'exitfullscreen': '退出全屏', 'toggledanmaku': '弹幕开关', 'screenshot': '截图', 'skip': '跳过', 'speed1': '倍速:1x', 'speed2': '倍速:2x', 'speed3': '倍速:3x', 'speedup': '倍速加', 'speeddown': '倍速减', }; ================================================ FILE: lib/utils/download_manager.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/modules/download/download_module.dart'; import 'package:kazumi/utils/m3u8_parser.dart'; import 'package:kazumi/utils/m3u8_ad_filter.dart'; import 'package:kazumi/utils/format_utils.dart' as fmt; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:path_provider/path_provider.dart'; class _NotM3u8Exception implements Exception { final String message; _NotM3u8Exception(this.message); @override String toString() => message; } class _InsufficientStorageException implements Exception { final int availableBytes; final int requiredBytes; _InsufficientStorageException(this.availableBytes, this.requiredBytes); @override String toString() => '存储空间不足'; } class DownloadTask { final String recordKey; final int episodeNumber; CancelToken cancelToken; bool isPaused; DownloadTask({ required this.recordKey, required this.episodeNumber, CancelToken? cancelToken, this.isPaused = false, }) : cancelToken = cancelToken ?? CancelToken(); } typedef ProgressCallback = void Function( String recordKey, int episodeNumber, DownloadEpisode episode, double speed); class DownloadRequest { final String recordKey; final int bangumiId; final String pluginName; final int episodeNumber; final String m3u8Url; final Map httpHeaders; final bool adBlockerEnabled; final DownloadEpisode episode; const DownloadRequest({ required this.recordKey, required this.bangumiId, required this.pluginName, required this.episodeNumber, required this.m3u8Url, required this.httpHeaders, required this.adBlockerEnabled, required this.episode, }); } abstract class IDownloadManager { ProgressCallback? onProgress; bool isDownloading(String recordKey, int episodeNumber); Future enqueue(DownloadRequest request); Future enqueuePriority(DownloadRequest request); void pause(String recordKey, int episodeNumber); Future resume(DownloadRequest request); void cancel(String recordKey, int episodeNumber); String? getLocalVideoPath(DownloadEpisode? episode); Future deleteEpisodeFiles(int bangumiId, String pluginName, int episodeNumber); Future deleteRecordFiles(int bangumiId, String pluginName); double getSpeed(String recordKey, int episodeNumber); } class _SpeedTracker { int _lastBytes = 0; DateTime _lastTime = DateTime.now(); double currentSpeed = 0.0; // bytes/sec void update(int totalBytes) { final now = DateTime.now(); final elapsed = now.difference(_lastTime).inMilliseconds; if (elapsed > 500) { final bytesDownloaded = totalBytes - _lastBytes; currentSpeed = bytesDownloaded / (elapsed / 1000); _lastBytes = totalBytes; _lastTime = now; } } void reset() { _lastBytes = 0; _lastTime = DateTime.now(); currentSpeed = 0.0; } } class DownloadManager implements IDownloadManager { DownloadManager() { _loadSettings(); } final Dio _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 30), )); final Map _activeTasks = {}; final List _queue = []; final Map _speedTrackers = {}; int maxParallelEpisodes = 2; int maxParallelSegments = 3; int _runningCount = 0; @override ProgressCallback? onProgress; static const _minRequiredSpace = 100 * 1024 * 1024; // 100MB minimum static const _storageChannel = MethodChannel('com.predidit.kazumi/storage'); void _loadSettings() { final setting = GStorage.setting; maxParallelEpisodes = setting.get( SettingBoxKey.downloadParallelEpisodes, defaultValue: 2, ); maxParallelSegments = setting.get( SettingBoxKey.downloadParallelSegments, defaultValue: 3, ); } /// Returns available bytes, or -1 if unable to determine Future _getAvailableStorage(String path) async { try { final result = await _storageChannel.invokeMethod( 'getAvailableStorage', {'path': path}, ); return result ?? -1; } on MissingPluginException { return -1; } catch (e) { KazumiLogger().w('DownloadManager: failed to get storage info', error: e); return -1; } } Future _checkStorageSpace(String downloadDir, {int requiredBytes = 0}) async { final available = await _getAvailableStorage(downloadDir); if (available == -1) return; // Skip check if unable to determine final required = requiredBytes > 0 ? requiredBytes : _minRequiredSpace; if (available < required) { throw _InsufficientStorageException(available, required); } } String _getStorageErrorMessage(FileSystemException e) { // POSIX error code 28 = ENOSPC (No space left on device) if (e.osError?.errorCode == 28) { return '存储空间不足,请清理后重试'; } // POSIX error code 13 = EACCES (Permission denied) if (e.osError?.errorCode == 13) { return '存储权限被拒绝'; } // POSIX error code 30 = EROFS (Read-only file system) if (e.osError?.errorCode == 30) { return '存储为只读,无法写入'; } return '存储错误: ${e.message}'; } @override double getSpeed(String recordKey, int episodeNumber) { final key = _taskKey(recordKey, episodeNumber); return _speedTrackers[key]?.currentSpeed ?? 0.0; } String _taskKey(String recordKey, int episodeNumber) => '${recordKey}_$episodeNumber'; @override bool isDownloading(String recordKey, int episodeNumber) => _activeTasks.containsKey(_taskKey(recordKey, episodeNumber)); Future get _downloadBaseDir async { final appSupport = await getApplicationSupportDirectory(); return '${appSupport.path}/downloads'; } String getEpisodeDir(String downloadBase, int bangumiId, String pluginName, int episodeNumber) { return '$downloadBase/${bangumiId}_$pluginName/$episodeNumber'; } @override Future enqueue(DownloadRequest request) async { final key = _taskKey(request.recordKey, request.episodeNumber); if (_activeTasks.containsKey(key)) return; final task = DownloadTask( recordKey: request.recordKey, episodeNumber: request.episodeNumber, ); if (_runningCount < maxParallelEpisodes) { _runningCount++; _activeTasks[key] = task; _runEpisodeDownload( task: task, bangumiId: request.bangumiId, pluginName: request.pluginName, m3u8Url: request.m3u8Url, httpHeaders: request.httpHeaders, adBlockerEnabled: request.adBlockerEnabled, episode: request.episode, ); } else { request.episode.status = DownloadStatus.pending; _queue.add(request); _activeTasks[key] = task; } } @override Future enqueuePriority(DownloadRequest request) async { final key = _taskKey(request.recordKey, request.episodeNumber); _queue.removeWhere( (r) => r.recordKey == request.recordKey && r.episodeNumber == request.episodeNumber, ); _activeTasks.remove(key); final task = DownloadTask( recordKey: request.recordKey, episodeNumber: request.episodeNumber, ); // Start immediately, bypassing the parallel limit (priority download) _runningCount++; _activeTasks[key] = task; _runEpisodeDownload( task: task, bangumiId: request.bangumiId, pluginName: request.pluginName, m3u8Url: request.m3u8Url, httpHeaders: request.httpHeaders, adBlockerEnabled: request.adBlockerEnabled, episode: request.episode, ); } @override void pause(String recordKey, int episodeNumber) { final key = _taskKey(recordKey, episodeNumber); final task = _activeTasks[key]; if (task != null) { task.isPaused = true; task.cancelToken.cancel('paused'); } } @override Future resume(DownloadRequest request) async { final key = _taskKey(request.recordKey, request.episodeNumber); _activeTasks.remove(key); final task = DownloadTask( recordKey: request.recordKey, episodeNumber: request.episodeNumber, ); _activeTasks[key] = task; if (_runningCount < maxParallelEpisodes) { _runningCount++; _runEpisodeDownload( task: task, bangumiId: request.bangumiId, pluginName: request.pluginName, m3u8Url: request.m3u8Url, httpHeaders: request.httpHeaders, adBlockerEnabled: request.adBlockerEnabled, episode: request.episode, ); } else { _queue.add(request); } } @override void cancel(String recordKey, int episodeNumber) { final key = _taskKey(recordKey, episodeNumber); final task = _activeTasks[key]; if (task != null) { task.cancelToken.cancel('cancelled'); _activeTasks.remove(key); _queue.removeWhere( (r) => r.recordKey == recordKey && r.episodeNumber == episodeNumber, ); } } void _processQueue() { while (_runningCount < maxParallelEpisodes && _queue.isNotEmpty) { final request = _queue.removeAt(0); final key = _taskKey(request.recordKey, request.episodeNumber); final existingTask = _activeTasks[key]; if (existingTask == null || existingTask.isPaused || existingTask.cancelToken.isCancelled) { _activeTasks.remove(key); continue; } _runningCount++; _runEpisodeDownload( task: existingTask, bangumiId: request.bangumiId, pluginName: request.pluginName, m3u8Url: request.m3u8Url, httpHeaders: request.httpHeaders, adBlockerEnabled: request.adBlockerEnabled, episode: request.episode, ); } } Future _runEpisodeDownload({ required DownloadTask task, required int bangumiId, required String pluginName, required String m3u8Url, required Map httpHeaders, required bool adBlockerEnabled, required DownloadEpisode episode, }) async { final key = _taskKey(task.recordKey, task.episodeNumber); try { episode.status = DownloadStatus.downloading; episode.networkM3u8Url = m3u8Url; _notifyProgress(task.recordKey, task.episodeNumber, episode); String m3u8Content; try { m3u8Content = await _fetchM3u8(m3u8Url, httpHeaders, task.cancelToken); } on _NotM3u8Exception { KazumiLogger().i( 'DownloadManager: URL is not M3U8, falling back to direct file download ' 'for episode ${task.episodeNumber}', ); await _runDirectFileDownload( task: task, bangumiId: bangumiId, pluginName: pluginName, videoUrl: m3u8Url, httpHeaders: httpHeaders, episode: episode, ); return; } final type = M3u8Parser.detectType(m3u8Content); String mediaM3u8Content = m3u8Content; String mediaM3u8Url = m3u8Url; if (type == M3u8Type.master) { final master = M3u8Parser.parseMasterPlaylist(m3u8Content, m3u8Url); final bestVariant = master.bestVariant; mediaM3u8Url = bestVariant.uri; mediaM3u8Content = await _fetchM3u8(mediaM3u8Url, httpHeaders, task.cancelToken); } final playlist = M3u8Parser.parseMediaPlaylist(mediaM3u8Content, mediaM3u8Url); // 展开嵌套 m3u8 片段(部分源将实际内容嵌套在 m3u8 引用中) final resolvedSegments = await M3u8Parser.resolveNestedSegments( playlist.segments, (url) => _fetchM3u8(url, httpHeaders, task.cancelToken), ); final resolvedPlaylist = M3u8MediaPlaylist( segments: resolvedSegments, targetDuration: playlist.targetDuration, isVod: playlist.isVod, ); if (!resolvedPlaylist.isVod) { episode.status = DownloadStatus.failed; episode.errorMessage = '不支持下载直播流 (无有效分片)'; _notifyProgress(task.recordKey, task.episodeNumber, episode); _onTaskComplete(key); return; } if (resolvedPlaylist.segments.isEmpty) { episode.status = DownloadStatus.failed; episode.errorMessage = 'M3U8 中未找到可下载的分片'; _notifyProgress(task.recordKey, task.episodeNumber, episode); _onTaskComplete(key); return; } List segments = resolvedPlaylist.segments; if (adBlockerEnabled) { segments = M3u8AdFilter.filterAds(segments); } final base = await _downloadBaseDir; final episodeDir = getEpisodeDir(base, bangumiId, pluginName, task.episodeNumber); await Directory(episodeDir).create(recursive: true); episode.downloadDirectory = episodeDir; await _checkStorageSpace(base); final keys = M3u8Parser.extractUniqueKeys( M3u8MediaPlaylist( segments: segments, targetDuration: resolvedPlaylist.targetDuration, isVod: true, ), ); final keyUriToLocal = {}; for (int i = 0; i < keys.length; i++) { final keyFile = 'key_$i.key'; final keyPath = '$episodeDir/$keyFile'; await _downloadFile(keys[i].uri, keyPath, httpHeaders, task.cancelToken); keyUriToLocal[keys[i].uri] = keyFile; } episode.totalSegments = segments.length; episode.downloadedSegments = 0; _notifyProgress(task.recordKey, task.episodeNumber, episode); final episodeDirObj = Directory(episodeDir); if (await episodeDirObj.exists()) { await for (final entity in episodeDirObj.list()) { if (entity.path.endsWith('.tmp')) { try { await entity.delete(); } catch (_) {} } } } final existingSegments = {}; for (int i = 0; i < segments.length; i++) { final segFile = File('$episodeDir/seg_${i.toString().padLeft(5, '0')}.ts'); if (await segFile.exists() && await segFile.length() > 0) { existingSegments.add(i); episode.downloadedSegments++; } } final pendingIndices = []; for (int i = 0; i < segments.length; i++) { if (!existingSegments.contains(i)) { pendingIndices.add(i); } } int totalBytes = 0; final completer = Completer(); int completedCount = 0; int failedCount = 0; final semaphore = _Semaphore(maxParallelSegments); _speedTrackers[key] = _SpeedTracker(); if (pendingIndices.isEmpty) { } else { for (final idx in pendingIndices) { if (task.isPaused || task.cancelToken.isCancelled) break; await semaphore.acquire(); if (task.isPaused || task.cancelToken.isCancelled) { semaphore.release(); break; } _downloadSegmentWithRetry( segments[idx].uri, '$episodeDir/seg_${idx.toString().padLeft(5, '0')}.ts', httpHeaders, task.cancelToken, ).then((bytes) { totalBytes += bytes; episode.downloadedSegments++; episode.totalBytes = totalBytes; episode.progressPercent = episode.downloadedSegments / episode.totalSegments; _speedTrackers[key]?.update(totalBytes); _notifyProgress(task.recordKey, task.episodeNumber, episode); completedCount++; semaphore.release(); if (completedCount + failedCount == pendingIndices.length) { completer.complete(); } }).catchError((e) { failedCount++; semaphore.release(); if (completedCount + failedCount == pendingIndices.length) { completer.complete(); } }); } if (!task.isPaused && !task.cancelToken.isCancelled && pendingIndices.isNotEmpty) { await completer.future; } } if (task.isPaused || task.cancelToken.isCancelled) { if (task.isPaused) { episode.status = DownloadStatus.paused; } _notifyProgress(task.recordKey, task.episodeNumber, episode); _onTaskComplete(key); return; } if (failedCount > 0) { episode.status = DownloadStatus.failed; episode.errorMessage = '$failedCount 个分片下载失败'; _notifyProgress(task.recordKey, task.episodeNumber, episode); _onTaskComplete(key); return; } final targetDuration = adBlockerEnabled ? M3u8AdFilter.calculateTargetDuration(segments) : resolvedPlaylist.targetDuration; final localM3u8 = M3u8Parser.buildLocalM3u8( segments, targetDuration: targetDuration, keyUriToLocal: keyUriToLocal, ); final m3u8Path = '$episodeDir/playlist.m3u8'; await File(m3u8Path).writeAsString(localM3u8); episode.status = DownloadStatus.completed; episode.localM3u8Path = m3u8Path; episode.progressPercent = 1.0; episode.completedAt = DateTime.now(); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().i( 'DownloadManager: episode ${task.episodeNumber} completed. ' '${segments.length} segments, ${(totalBytes / 1024 / 1024).toStringAsFixed(1)} MB', ); } on _InsufficientStorageException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})'; _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().w('DownloadManager: insufficient storage space', error: e); } on FileSystemException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = _getStorageErrorMessage(e); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().e('DownloadManager: file system error', error: e); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) { if (task.isPaused) { episode.status = DownloadStatus.paused; } } else { episode.status = DownloadStatus.failed; episode.errorMessage = e.message ?? '网络错误'; } _notifyProgress(task.recordKey, task.episodeNumber, episode); } catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = e.toString(); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().e('DownloadManager: episode download failed', error: e); } finally { _onTaskComplete(key); } } Future _runDirectFileDownload({ required DownloadTask task, required int bangumiId, required String pluginName, required String videoUrl, required Map httpHeaders, required DownloadEpisode episode, }) async { final key = _taskKey(task.recordKey, task.episodeNumber); try { final base = await _downloadBaseDir; final episodeDir = getEpisodeDir(base, bangumiId, pluginName, task.episodeNumber); await Directory(episodeDir).create(recursive: true); episode.downloadDirectory = episodeDir; await _checkStorageSpace(base); final filePath = '$episodeDir/video.mp4'; final tmpPath = '$filePath.tmp'; int existingBytes = 0; final tmpFile = File(tmpPath); if (await tmpFile.exists()) { existingBytes = await tmpFile.length(); } episode.totalSegments = 1; episode.downloadedSegments = 0; _notifyProgress(task.recordKey, task.episodeNumber, episode); final requestHeaders = Map.from(httpHeaders); bool useRange = existingBytes > 0; if (useRange) { requestHeaders['Range'] = 'bytes=$existingBytes-'; } Response response; try { response = await _dio.get( videoUrl, options: Options( headers: requestHeaders, responseType: ResponseType.stream, receiveTimeout: const Duration(minutes: 30), ), cancelToken: task.cancelToken, ); } on DioException catch (e) { if (e.response?.statusCode == 416 && useRange) { KazumiLogger().w( 'DownloadManager: 416 Range Not Satisfiable, deleting tmp file and retrying', ); await tmpFile.delete(); existingBytes = 0; requestHeaders.remove('Range'); response = await _dio.get( videoUrl, options: Options( headers: requestHeaders, responseType: ResponseType.stream, receiveTimeout: const Duration(minutes: 30), ), cancelToken: task.cancelToken, ); } else { rethrow; } } final contentRange = response.headers.value('content-range'); final contentLength = int.tryParse( response.headers.value(Headers.contentLengthHeader) ?? '') ?? 0; int totalSize; if (contentRange != null) { final totalMatch = RegExp(r'/(\d+)').firstMatch(contentRange); totalSize = totalMatch != null ? int.parse(totalMatch.group(1)!) : 0; } else { totalSize = existingBytes + contentLength; } final raf = await tmpFile.open( mode: existingBytes > 0 ? FileMode.append : FileMode.write); int received = existingBytes; _speedTrackers[key] = _SpeedTracker(); try { await for (final chunk in response.data!.stream) { if (task.isPaused || task.cancelToken.isCancelled) break; await raf.writeFrom(chunk); received += chunk.length; episode.totalBytes = received; episode.progressPercent = totalSize > 0 ? received / totalSize : 0; // Update speed tracker _speedTrackers[key]?.update(received); _notifyProgress(task.recordKey, task.episodeNumber, episode); } } finally { await raf.close(); } if (task.isPaused || task.cancelToken.isCancelled) { if (task.isPaused) { episode.status = DownloadStatus.paused; } _notifyProgress(task.recordKey, task.episodeNumber, episode); _onTaskComplete(key); return; } await File(tmpPath).rename(filePath); episode.status = DownloadStatus.completed; episode.localM3u8Path = filePath; episode.downloadedSegments = 1; episode.progressPercent = 1.0; episode.completedAt = DateTime.now(); episode.totalBytes = await File(filePath).length(); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().i( 'DownloadManager: episode ${task.episodeNumber} completed (direct download). ' '${(episode.totalBytes / 1024 / 1024).toStringAsFixed(1)} MB', ); } on _InsufficientStorageException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})'; _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().w('DownloadManager: insufficient storage space', error: e); } on FileSystemException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = _getStorageErrorMessage(e); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().e('DownloadManager: file system error', error: e); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) { if (task.isPaused) { episode.status = DownloadStatus.paused; } } else { episode.status = DownloadStatus.failed; episode.errorMessage = e.message ?? '网络错误'; } _notifyProgress(task.recordKey, task.episodeNumber, episode); } catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = e.toString(); _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().e('DownloadManager: direct file download failed', error: e); } finally { _onTaskComplete(key); } } void _onTaskComplete(String key) { _activeTasks.remove(key); _speedTrackers.remove(key); _runningCount--; _processQueue(); } void _notifyProgress( String recordKey, int episodeNumber, DownloadEpisode episode) { final key = _taskKey(recordKey, episodeNumber); final speed = _speedTrackers[key]?.currentSpeed ?? 0.0; onProgress?.call(recordKey, episodeNumber, episode, speed); } Future _fetchM3u8( String url, Map headers, CancelToken cancelToken) async { final fetchToken = CancelToken(); if (cancelToken.isCancelled) { throw DioException( type: DioExceptionType.cancel, requestOptions: RequestOptions(path: url), ); } try { final response = await _dio.get( url, options: Options( headers: headers, responseType: ResponseType.plain, receiveTimeout: const Duration(seconds: 15), ), cancelToken: fetchToken, onReceiveProgress: (received, total) { if (cancelToken.isCancelled) { fetchToken.cancel('task cancelled'); return; } if (received > 2 * 1024 * 1024) { fetchToken.cancel('too large'); } }, ); final content = response.data!; final trimmed = content.trimLeft(); if (!trimmed.startsWith('#EXTM3U')) { throw _NotM3u8Exception('URL 不是 M3U8 播放列表'); } return content; } on DioException catch (e) { if (cancelToken.isCancelled) rethrow; if (e.type == DioExceptionType.cancel) { throw _NotM3u8Exception('响应过大,非 M3U8 播放列表'); } rethrow; } } Future _downloadFile(String url, String savePath, Map headers, CancelToken cancelToken) async { await _dio.download( url, savePath, options: Options(headers: headers), cancelToken: cancelToken, ); } Future _downloadSegmentWithRetry( String url, String savePath, Map headers, CancelToken cancelToken, { int maxRetries = 3, }) async { final tmpPath = '$savePath.tmp'; int retryCount = 0; while (true) { try { await _dio.download( url, tmpPath, options: Options(headers: headers), cancelToken: cancelToken, ); await File(tmpPath).rename(savePath); return await File(savePath).length(); } catch (e) { try { final tmpFile = File(tmpPath); if (await tmpFile.exists()) await tmpFile.delete(); } catch (_) {} if (cancelToken.isCancelled) rethrow; retryCount++; if (retryCount >= maxRetries) rethrow; final delay = Duration(seconds: [1, 3, 9][retryCount - 1]); await Future.delayed(delay); } } } @override Future deleteEpisodeFiles( int bangumiId, String pluginName, int episodeNumber) async { final base = await _downloadBaseDir; final dir = Directory(getEpisodeDir(base, bangumiId, pluginName, episodeNumber)); if (await dir.exists()) { await dir.delete(recursive: true); } } @override Future deleteRecordFiles(int bangumiId, String pluginName) async { final base = await _downloadBaseDir; final dir = Directory('$base/${bangumiId}_$pluginName'); if (await dir.exists()) { await dir.delete(recursive: true); } } @override String? getLocalVideoPath(DownloadEpisode? episode) { if (episode == null) return null; if (episode.status != DownloadStatus.completed) return null; if (episode.localM3u8Path.isEmpty) return null; final file = File(episode.localM3u8Path); if (!file.existsSync()) return null; return episode.localM3u8Path; } } class _Semaphore { final int maxCount; int _currentCount = 0; final _waitQueue = >[]; _Semaphore(this.maxCount); Future acquire() async { if (_currentCount < maxCount) { _currentCount++; return; } final completer = Completer(); _waitQueue.add(completer); return completer.future; } void release() { if (_waitQueue.isNotEmpty) { final completer = _waitQueue.removeAt(0); completer.complete(); } else { _currentCount--; } } } ================================================ FILE: lib/utils/extension.dart ================================================ import 'package:flutter/material.dart'; extension ImageExtension on num { int cacheSize(BuildContext context) { return (this * MediaQuery.of(context).devicePixelRatio).round(); } } ================================================ FILE: lib/utils/external_player.dart ================================================ import 'package:flutter/services.dart'; import 'package:kazumi/utils/logger.dart'; class ExternalPlayer { // 注意:仍需开发 iOS/Linux 设备的外部播放功能。 // 在 Windows 设备上,对于其他可能的实现,使用 scheme 的方案没有效果。VLC / PotPlayer 等主流播放器更倾向于使用 CLI 命令。 // 可行的 iOS 处理代码,请参见 ios/Runner/AppDelegate.swift 的注释部分。 static const platform = MethodChannel('com.predidit.kazumi/intent'); static Future launchURLWithMIME(String url, String mimeType) async { try { await platform.invokeMethod( 'openWithMime', {'url': url, 'mimeType': mimeType}); return true; } on PlatformException catch (e) { KazumiLogger() .e("ExternalPlayer: failed to open with mime", error: e); return false; } } static Future launchURLWithReferer(String url, String referer) async { try { await platform.invokeMethod( 'openWithReferer', {'url': url, 'referer': referer}); return true; } on PlatformException catch (e) { KazumiLogger() .e("ExternalPlayer: failed to open with referer", error: e); return false; } } } ================================================ FILE: lib/utils/format_utils.dart ================================================ String formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; if (bytes < 1024 * 1024 * 1024) { return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB'; } return '${(bytes / 1024 / 1024 / 1024).toStringAsFixed(1)} GB'; } String formatSpeed(double bytesPerSec) { if (bytesPerSec < 1024) return '${bytesPerSec.toStringAsFixed(0)} B/s'; if (bytesPerSec < 1024 * 1024) { return '${(bytesPerSec / 1024).toStringAsFixed(1)} KB/s'; } return '${(bytesPerSec / 1024 / 1024).toStringAsFixed(1)} MB/s'; } ================================================ FILE: lib/utils/logger.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:logger/logger.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:synchronized/synchronized.dart'; const Symbol _forceLogKey = #_forceLog; class KazumiLogFilter extends LogFilter { @override bool shouldLog(LogEvent event) { final forceLog = Zone.current[_forceLogKey] as bool? ?? false; if (forceLog) { return true; } return event.level.index >= Logger.level.index; } } class KazumiLogPrinter extends PrettyPrinter { KazumiLogPrinter() : super( methodCount: 0, errorMethodCount: 8, lineLength: 120, colors: true, // Disable emojis for better compatibility printEmojis: false, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, ); @override List log(LogEvent event) { // For trace, debug, info - never show stack trace if (event.level == Level.trace || event.level == Level.debug || event.level == Level.info) { final messageStr = stringifyMessage(event.message); final time = getTime(event.time); final prefix = _getPrefix(event.level); final levelName = _getLevelName(event.level); return [ '$prefix $time $levelName $messageStr', ]; } // For warning, error, fatal - use default behavior which shows stack if provided return super.log(event); } /// Colored prefix for log level String _getPrefix(Level level) { if (!colors) return _getLevelTag(level); const reset = '\x1B[0m'; String colorCode; switch (level) { case Level.trace: colorCode = '\x1B[90m'; // Bright Black case Level.debug: colorCode = '\x1B[36m'; // Cyan case Level.info: colorCode = '\x1B[32m'; // Green case Level.warning: colorCode = '\x1B[33m'; // Yellow case Level.error: colorCode = '\x1B[31m'; // Red case Level.fatal: colorCode = '\x1B[35m'; // Magenta default: colorCode = ''; } return '$colorCode${_getLevelTag(level)}$reset'; } /// Tag symbol for log level String _getLevelTag(Level level) { switch (level) { case Level.trace: return '[·]'; case Level.debug: return '[*]'; case Level.info: return '[i]'; case Level.warning: return '[!]'; case Level.error: return '[×]'; case Level.fatal: return '[‼]'; default: return '[-]'; } } String _getLevelName(Level level) { return level.name.toUpperCase().padRight(7); } } class KazumiLogOutput extends LogOutput { static final Lock _logLock = Lock(); static String? _logFilePath; static Future _getLogFilePath() async { if (_logFilePath != null) return _logFilePath!; final dir = (await getApplicationSupportDirectory()).path; final logDir = p.join(dir, "logs"); final directory = Directory(logDir); if (!await directory.exists()) { await directory.create(recursive: true); } _logFilePath = p.join(logDir, "kazumi_logs.log"); return _logFilePath!; } @override void output(OutputEvent event) { for (var line in event.lines) { print(line); } // Write to file if: warning/error/fatal OR forceLog is enabled final forceLog = Zone.current[_forceLogKey] as bool? ?? false; if (event.level.index >= Level.warning.index || forceLog) { _writeToFile(event); } } void _writeToFile(OutputEvent event) { _logLock.synchronized(() async { try { final filePath = await _getLogFilePath(); final file = File(filePath); final timestamp = DateTime.now().toString(); final buffer = StringBuffer(); buffer.writeln('[$timestamp]'); for (var line in event.lines) { final cleanLine = _removeAnsiCodes(line); buffer.writeln(cleanLine); } buffer.writeln(); await file.writeAsString( buffer.toString(), mode: FileMode.writeOnlyAppend, ); } catch (e) { print('Failed to write log to file: $e'); } }); } /// Remove ANSI escape codes from string to ensure clean log files String _removeAnsiCodes(String text) { return text.replaceAll(RegExp(r'\x1B\[[0-9;]*m'), ''); } } class KazumiLogger { KazumiLogger._internal() { _logger = Logger( filter: KazumiLogFilter(), printer: KazumiLogPrinter(), output: KazumiLogOutput(), ); } static final KazumiLogger _instance = KazumiLogger._internal(); factory KazumiLogger() { return _instance; } late final Logger _logger; void _log(void Function() logFn, bool forceLog) { if (forceLog) { runZoned(logFn, zoneValues: {_forceLogKey: true}); } else { logFn(); } } /// Trace log - lowest level, very detailed information void t(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.t(message, error: error, stackTrace: stackTrace), forceLog); } /// Debug log - detailed information for debugging void d(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.d(message, error: error, stackTrace: stackTrace), forceLog); } /// Info log - informational messages void i(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.i(message, error: error, stackTrace: stackTrace), forceLog); } /// Warning log - potentially harmful situations void w(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.w(message, error: error, stackTrace: stackTrace), forceLog); } /// Error log - error events that might still allow the app to continue void e(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.e(message, error: error, stackTrace: stackTrace), forceLog); } /// Fatal log - very severe error events that will presumably lead the app to abort void f(dynamic message, {Object? error, StackTrace? stackTrace, bool forceLog = false}) { _log(() => _logger.f(message, error: error, stackTrace: stackTrace), forceLog); } } Future getLogsPath() async { final dir = (await getApplicationSupportDirectory()).path; final logDir = p.join(dir, "logs"); final filename = p.join(logDir, "kazumi_logs.log"); final directory = Directory(logDir); if (!await directory.exists()) { await directory.create(recursive: true); } final file = File(filename); if (!await file.exists()) { await KazumiLogOutput._logLock.synchronized(() async { if (!await file.exists()) { await file.create(); } }); } return file; } Future clearLogs() async { try { final file = await getLogsPath(); await KazumiLogOutput._logLock.synchronized(() async { await file.writeAsString(''); }); return true; } catch (e) { print('Error clearing file: $e'); return false; } } ================================================ FILE: lib/utils/m3u8_ad_filter.dart ================================================ import 'package:kazumi/utils/m3u8_parser.dart'; class M3u8AdFilter { /// Filter ad segments from a media playlist. /// Mimics FFmpeg hls_ad_filter behavior using discontinuity groups. static List filterAds(List segments) { if (segments.isEmpty) return segments; // Group segments by discontinuityGroup final groups = >{}; for (final seg in segments) { groups.putIfAbsent(seg.discontinuityGroup, () => []); groups[seg.discontinuityGroup]!.add(seg); } // Only one group means no ads detected if (groups.length <= 1) return segments; // Calculate total duration per group final groupDurations = {}; for (final entry in groups.entries) { groupDurations[entry.key] = entry.value.fold( 0.0, (sum, seg) => sum + seg.duration, ); } // Find the longest group as the "main content" reference double maxDuration = 0; for (final d in groupDurations.values) { if (d > maxDuration) maxDuration = d; } // Identify ad groups final adGroups = {}; final sortedKeys = groups.keys.toList()..sort(); for (final groupId in sortedKeys) { final groupDuration = groupDurations[groupId]!; // Skip the main content group if (groupDuration == maxDuration) continue; bool isAd = false; // Short segments relative to main content (< 30%) if (groupDuration < maxDuration * 0.3) { isAd = true; } // First or last group with short duration (< 30s) if ((groupId == sortedKeys.first || groupId == sortedKeys.last) && groupDuration < 30.0) { isAd = true; } // Very short segments (< 10s) are almost certainly ads if (groupDuration < 10.0) { isAd = true; } if (isAd) { adGroups.add(groupId); } } if (adGroups.isEmpty) return segments; // Remove ad segments return segments .where((seg) => !adGroups.contains(seg.discontinuityGroup)) .toList(); } /// Calculate the new target duration after filtering static double calculateTargetDuration(List segments) { if (segments.isEmpty) return 0; double maxSegDuration = 0; for (final seg in segments) { if (seg.duration > maxSegDuration) { maxSegDuration = seg.duration; } } return maxSegDuration; } } ================================================ FILE: lib/utils/m3u8_parser.dart ================================================ class M3u8Key { final String method; final String uri; final String? iv; M3u8Key({required this.method, required this.uri, this.iv}); @override bool operator ==(Object other) => identical(this, other) || other is M3u8Key && method == other.method && uri == other.uri && iv == other.iv; @override int get hashCode => Object.hash(method, uri, iv); @override String toString() { final sb = StringBuffer('#EXT-X-KEY:METHOD=$method,URI="$uri"'); if (iv != null) { sb.write(',IV=$iv'); } return sb.toString(); } } class M3u8Segment { final double duration; final String uri; final int discontinuityGroup; final M3u8Key? key; M3u8Segment({ required this.duration, required this.uri, required this.discontinuityGroup, this.key, }); } class M3u8Variant { final int bandwidth; final String? resolution; final String uri; M3u8Variant({required this.bandwidth, this.resolution, required this.uri}); } class M3u8MasterPlaylist { final List variants; M3u8MasterPlaylist({required this.variants}); M3u8Variant get bestVariant { return variants.reduce((a, b) => a.bandwidth > b.bandwidth ? a : b); } } class M3u8MediaPlaylist { final List segments; final double targetDuration; final bool isVod; M3u8MediaPlaylist({ required this.segments, required this.targetDuration, required this.isVod, }); } enum M3u8Type { master, media } class M3u8Parser { static M3u8Type detectType(String content) { if (content.contains('#EXT-X-STREAM-INF')) { return M3u8Type.master; } return M3u8Type.media; } static String resolveUrl(String baseUrl, String relativeUrl) { if (relativeUrl.startsWith('http://') || relativeUrl.startsWith('https://')) { return relativeUrl; } final baseUri = Uri.parse(baseUrl); if (relativeUrl.startsWith('/')) { return '${baseUri.scheme}://${baseUri.host}${baseUri.hasPort ? ':${baseUri.port}' : ''}$relativeUrl'; } final basePath = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1); return '$basePath$relativeUrl'; } static M3u8MasterPlaylist parseMasterPlaylist(String content, String baseUrl) { final lines = content.split('\n').map((l) => l.trim()).toList(); final variants = []; for (int i = 0; i < lines.length; i++) { final line = lines[i]; if (line.startsWith('#EXT-X-STREAM-INF:')) { final attrs = line.substring('#EXT-X-STREAM-INF:'.length); int bandwidth = 0; String? resolution; final bandwidthMatch = RegExp(r'BANDWIDTH=(\d+)').firstMatch(attrs); if (bandwidthMatch != null) { bandwidth = int.parse(bandwidthMatch.group(1)!); } final resolutionMatch = RegExp(r'RESOLUTION=([^\s,]+)').firstMatch(attrs); if (resolutionMatch != null) { resolution = resolutionMatch.group(1); } if (i + 1 < lines.length && !lines[i + 1].startsWith('#')) { final uri = resolveUrl(baseUrl, lines[i + 1]); variants.add(M3u8Variant(bandwidth: bandwidth, resolution: resolution, uri: uri)); } } } return M3u8MasterPlaylist(variants: variants); } static M3u8MediaPlaylist parseMediaPlaylist(String content, String baseUrl) { final lines = content.split('\n').map((l) => l.trim()).toList(); final segments = []; double targetDuration = 0; bool hasEndList = false; bool isExplicitVod = false; bool isLiveEvent = false; int currentDiscontinuityGroup = 0; M3u8Key? currentKey; double currentDuration = 0; for (int i = 0; i < lines.length; i++) { final line = lines[i]; if (line.startsWith('#EXT-X-TARGETDURATION:')) { targetDuration = double.parse(line.substring('#EXT-X-TARGETDURATION:'.length)); } else if (line == '#EXT-X-ENDLIST') { hasEndList = true; } else if (line == '#EXT-X-PLAYLIST-TYPE:VOD') { isExplicitVod = true; } else if (line == '#EXT-X-PLAYLIST-TYPE:EVENT') { isLiveEvent = true; } else if (line == '#EXT-X-DISCONTINUITY') { currentDiscontinuityGroup++; } else if (line.startsWith('#EXT-X-KEY:')) { currentKey = _parseKey(line, baseUrl); } else if (line.startsWith('#EXTINF:')) { final durationStr = line.substring('#EXTINF:'.length).split(',')[0]; currentDuration = double.parse(durationStr); } else if (line.isNotEmpty && !line.startsWith('#')) { final uri = resolveUrl(baseUrl, line); segments.add(M3u8Segment( duration: currentDuration, uri: uri, discontinuityGroup: currentDiscontinuityGroup, key: currentKey, )); currentDuration = 0; } } // Consider it VOD if: // 1. Has #EXT-X-ENDLIST, or // 2. Has #EXT-X-PLAYLIST-TYPE:VOD, or // 3. Has finite segments and is not explicitly a live EVENT stream. // Many third-party video sources omit #EXT-X-ENDLIST for VOD content. final bool isVod = hasEndList || isExplicitVod || (!isLiveEvent && segments.isNotEmpty); return M3u8MediaPlaylist( segments: segments, targetDuration: targetDuration, isVod: isVod, ); } static M3u8Key? _parseKey(String line, String baseUrl) { final attrs = line.substring('#EXT-X-KEY:'.length); final methodMatch = RegExp(r'METHOD=([^,]+)').firstMatch(attrs); final method = methodMatch?.group(1) ?? 'NONE'; if (method == 'NONE') return null; final uriMatch = RegExp(r'URI="([^"]+)"').firstMatch(attrs); final uri = uriMatch != null ? resolveUrl(baseUrl, uriMatch.group(1)!) : ''; final ivMatch = RegExp(r'IV=(0x[0-9a-fA-F]+)').firstMatch(attrs); final iv = ivMatch?.group(1); return M3u8Key(method: method, uri: uri, iv: iv); } static List extractUniqueKeys(M3u8MediaPlaylist playlist) { final seen = {}; final keys = []; for (final seg in playlist.segments) { if (seg.key != null && !seen.contains(seg.key!.uri)) { seen.add(seg.key!.uri); keys.add(seg.key!); } } return keys; } static String buildLocalM3u8( List segments, { required double targetDuration, Map keyUriToLocal = const {}, }) { final sb = StringBuffer(); sb.writeln('#EXTM3U'); sb.writeln('#EXT-X-VERSION:3'); sb.writeln('#EXT-X-TARGETDURATION:${targetDuration.ceil()}'); sb.writeln('#EXT-X-MEDIA-SEQUENCE:0'); int lastDiscontinuityGroup = 0; M3u8Key? lastKey; for (int i = 0; i < segments.length; i++) { final seg = segments[i]; if (seg.discontinuityGroup != lastDiscontinuityGroup && i > 0) { sb.writeln('#EXT-X-DISCONTINUITY'); lastDiscontinuityGroup = seg.discontinuityGroup; } if (seg.key != lastKey) { if (seg.key == null) { sb.writeln('#EXT-X-KEY:METHOD=NONE'); } else { final localUri = keyUriToLocal[seg.key!.uri] ?? seg.key!.uri; final keySb = StringBuffer('#EXT-X-KEY:METHOD=${seg.key!.method},URI="$localUri"'); if (seg.key!.iv != null) { keySb.write(',IV=${seg.key!.iv}'); } sb.writeln(keySb.toString()); } lastKey = seg.key; } sb.writeln('#EXTINF:${seg.duration.toStringAsFixed(6)},'); sb.writeln('seg_${i.toString().padLeft(5, '0')}.ts'); } sb.writeln('#EXT-X-ENDLIST'); return sb.toString(); } static bool _isM3u8Url(String url) { final path = Uri.parse(url).path.toLowerCase(); return path.endsWith('.m3u8'); } /// 展开嵌套 M3U8 片段。 /// [fetcher] 异步回调,给定 URL 返回 M3U8 文本内容。 /// [maxDepth] 递归深度上限,防止无限嵌套。 static Future> resolveNestedSegments( List segments, Future Function(String url) fetcher, { int maxDepth = 3, }) async { if (maxDepth <= 0) return segments; if (!segments.any((s) => _isM3u8Url(s.uri))) return segments; final result = []; int groupOffset = 0; for (final seg in segments) { if (!_isM3u8Url(seg.uri)) { result.add(M3u8Segment( duration: seg.duration, uri: seg.uri, discontinuityGroup: seg.discontinuityGroup + groupOffset, key: seg.key, )); continue; } // 该 segment 的 URI 指向嵌套 m3u8,展开 try { final content = await fetcher(seg.uri); final nested = parseMediaPlaylist(content, seg.uri); final resolved = await resolveNestedSegments( nested.segments, fetcher, maxDepth: maxDepth - 1, ); if (resolved.isEmpty) continue; final nestedBase = seg.discontinuityGroup + groupOffset; int maxNestedGroup = 0; for (final ns in resolved) { if (ns.discontinuityGroup > maxNestedGroup) { maxNestedGroup = ns.discontinuityGroup; } } for (final ns in resolved) { result.add(M3u8Segment( duration: ns.duration, uri: ns.uri, discontinuityGroup: ns.discontinuityGroup + nestedBase, key: ns.key, )); } // 后续 segment 的 group 需要额外偏移,避免碰撞 groupOffset += maxNestedGroup; } catch (e) { // 获取/解析失败,保留原始 segment result.add(M3u8Segment( duration: seg.duration, uri: seg.uri, discontinuityGroup: seg.discontinuityGroup + groupOffset, key: seg.key, )); } } return result; } } ================================================ FILE: lib/utils/mortis.dart ================================================ // The file contains some interesting imformation about dandan public api. // Unusual name to avoid github global search, it's just test code and will be replaced in github actions. const Map mortis = { 'id': 'kvpx7qkqjh', 'value': 'rABUaBLqdz7aCSi3fe88ZDj2gwga9Vax', }; ================================================ FILE: lib/utils/proxy_manager.dart ================================================ import 'package:kazumi/request/request.dart'; /// 代理管理器 /// 统一管理 Dio HTTP 请求的代理设置 /// 注意:WebView 代理在各平台 controller 初始化时单独处理 class ProxyManager { ProxyManager._(); /// 应用代理设置 static void applyProxy() { Request.setProxy(); } /// 清除代理设置 static void clearProxy() { Request.disableProxy(); } } ================================================ FILE: lib/utils/proxy_utils.dart ================================================ /// 代理相关的工具函数 class ProxyUtils { // 防止实例化 ProxyUtils._(); /// 解析代理 URL,返回 (主机, 端口) /// /// 支持的格式: /// - http://127.0.0.1:7890 /// - 127.0.0.1:7890 static (String, int)? parseProxyUrl(String url) { url = url.trim(); if (url.isEmpty) return null; String hostPort = url; // 移除 http:// 前缀 if (url.toLowerCase().startsWith('http://')) { hostPort = url.substring(7); } else if (url.toLowerCase().startsWith('https://')) { hostPort = url.substring(8); } // 解析主机和端口 final parts = hostPort.split(':'); if (parts.length != 2) return null; final host = parts[0]; final port = int.tryParse(parts[1]); if (host.isEmpty || port == null) return null; return (host, port); } /// 获取格式化的代理 URL(用于 mpv) static String? getFormattedProxyUrl(String url) { final parsed = parseProxyUrl(url); if (parsed == null) return null; return 'http://${parsed.$1}:${parsed.$2}'; } /// 验证代理 URL 是否有效 static bool isValidProxyUrl(String url) { return parseProxyUrl(url) != null; } } ================================================ FILE: lib/utils/remote.dart ================================================ import 'dart:async'; import 'package:dlna_dart/dlna.dart'; import 'package:flutter/material.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/utils/logger.dart'; class RemotePlay { Future castVideo(String video, String referer) async { final searcher = DLNAManager(); final dlna = await searcher.start(); List dlnaDevice = []; await KazumiDialog.show(builder: (BuildContext context) { return StatefulBuilder(builder: (context, setState) { return AlertDialog( title: const Text('远程投屏'), content: SingleChildScrollView( child: Column( children: dlnaDevice, ), ), actions: [ const SizedBox(width: 20), TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( '退出', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { setState(() {}); KazumiDialog.showToast( message: '开始搜索', ); dlna.devices.stream.listen((deviceList) { dlnaDevice = []; deviceList.forEach((key, value) async { KazumiLogger().i('RemotePlay: key: $key'); KazumiLogger().i( 'RemotePlay: value: ${value.info.friendlyName} ${value.info.deviceType} ${value.info.URLBase}'); setState(() { dlnaDevice.add(ListTile( leading: _deviceUPnPIcon( value.info.deviceType.split(':')[3]), title: Text(value.info.friendlyName), subtitle: Text(value.info.deviceType.split(':')[3]), onTap: () { try { KazumiDialog.showToast( message: '尝试投屏至 ${value.info.friendlyName}', ); DLNADevice(value.info).setUrl(video); DLNADevice(value.info).play(); } catch (e) { KazumiLogger() .e('RemotePlay: failed to cast to device', error: e); KazumiDialog.showToast( message: 'DLNA 异常: $e \n尝试重新进入 DLNA 投屏或切换设备', ); } })); }); }); }); // Timer(const Duration(seconds: 30), () { // KazumiDialog.showToast( // message: '已搜索30s,若未发现设备请尝试重新进入 DLNA 投屏', // ); // }); }, child: Text( '搜索', style: TextStyle(color: Theme.of(context).colorScheme.outline), )), ], ); }); }, onDismiss: () { searcher.stop(); }); } Icon _deviceUPnPIcon(String deviceType) { switch (deviceType) { case 'MediaRenderer': return const Icon(Icons.cast_connected); case 'MediaServer': return const Icon(Icons.cast_connected); case 'InternetGatewayDevice': return const Icon(Icons.router); case 'BasicDevice': return const Icon(Icons.device_hub); case 'DimmableLight': return const Icon(Icons.lightbulb); case 'WLANAccessPoint': return const Icon(Icons.lan); case 'WLANConnectionDevice': return const Icon(Icons.wifi_tethering); case 'Printer': return const Icon(Icons.print); case 'Scanner': return const Icon(Icons.scanner); case 'DigitalSecurityCamera': return const Icon(Icons.camera_enhance_outlined); default: return const Icon(Icons.question_mark); } } } ================================================ FILE: lib/utils/search_parser.dart ================================================ class SearchParser { final String query; final RegExp _idRegExp = RegExp(r'id:(\d+)', caseSensitive: false); final RegExp _tagRegExp = RegExp(r'tag:([\w\u4e00-\u9fa5\u30A0-\u30FF\.\-]+)', caseSensitive: false); final RegExp _sortRegExp = RegExp(r'sort:([\w\-]+)', caseSensitive: false); SearchParser(this.query); String? parseId() { final match = _idRegExp.firstMatch(query); return match != null ? match.group(1) : null; } String? parseTag() { final match = _tagRegExp.firstMatch(query); return match != null ? match.group(1) : null; } String? parseSort() { final match = _sortRegExp.firstMatch(query); return match != null ? match.group(1) : null; } String parseKeywords() { String cleaned = query.replaceAll(_idRegExp, ''); cleaned = cleaned.replaceAll(_tagRegExp, ''); cleaned = cleaned.replaceAll(_sortRegExp, ''); return cleaned.trim(); } bool hasSortSyntax() { return _sortRegExp.hasMatch(query); } String removeSort() { return query.replaceAll(_sortRegExp, '').trim(); } String updateSort(String sortValue) { if (hasSortSyntax()) { return query.replaceAllMapped(_sortRegExp, (match) => 'sort:$sortValue'); } else { return '${query.trim()} sort:$sortValue'.trim(); } } } ================================================ FILE: lib/utils/storage.dart ================================================ import 'dart:io'; import 'package:hive_ce/hive.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/modules/bangumi/bangumi_tag.dart'; import 'package:kazumi/modules/history/history_module.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:kazumi/modules/collect/collect_change_module.dart'; import 'package:kazumi/modules/search/search_history_module.dart'; import 'package:kazumi/modules/download/download_module.dart'; class GStorage { /// Don't use favorites box, it's replaced by collectibles. static late Box favorites; static late Box collectibles; static late Box histories; static late Box collectChanges; static late Box shieldList; static late final Box setting; static late Box searchHistory; static late Box downloads; /// Hive directory path, initialized during init() static String? _hivePath; static Future init() async { _hivePath = '${(await getApplicationSupportDirectory()).path}/hive'; Hive.registerAdapter(BangumiItemAdapter()); Hive.registerAdapter(BangumiTagAdapter()); Hive.registerAdapter(CollectedBangumiAdapter()); Hive.registerAdapter(ProgressAdapter()); Hive.registerAdapter(HistoryAdapter()); Hive.registerAdapter(CollectedBangumiChangeAdapter()); Hive.registerAdapter(SearchHistoryAdapter()); Hive.registerAdapter(DownloadRecordAdapter()); Hive.registerAdapter(DownloadEpisodeAdapter()); // Open each box with automatic recovery on corruption favorites = await _openBoxSafe('favorites'); collectibles = await _openBoxSafe('collectibles'); histories = await _openBoxSafe('histories'); setting = await _openBoxSafe('setting'); collectChanges = await _openBoxSafe('collectchanges'); shieldList = await _openBoxSafe('shieldList'); searchHistory = await _openBoxSafe('searchHistory'); downloads = await _openBoxSafe('downloads'); } /// Open a Hive box with automatic recovery on corruption. /// If the box is corrupted, delete it and create a new empty one. static Future> _openBoxSafe(String boxName) async { try { return await Hive.openBox(boxName); } catch (e) { KazumiLogger().e('GStorage: Box "$boxName" corrupted, attempting recovery', error: e); // Delete the corrupted box files await _deleteBoxFiles(boxName); // Try to open again (will create a new empty box) try { final box = await Hive.openBox(boxName); KazumiLogger().i('GStorage: Box "$boxName" recovered successfully (data lost)'); return box; } catch (e2) { KazumiLogger().e('GStorage: Failed to recover box "$boxName"', error: e2); rethrow; } } } /// Delete Hive box files for a given box name static Future _deleteBoxFiles(String boxName) async { if (_hivePath == null) return; final boxFile = File('$_hivePath/$boxName.hive'); final lockFile = File('$_hivePath/$boxName.lock'); try { if (await boxFile.exists()) { await boxFile.delete(); KazumiLogger().i('GStorage: Deleted corrupted box file: $boxName.hive'); } if (await lockFile.exists()) { await lockFile.delete(); KazumiLogger().i('GStorage: Deleted lock file: $boxName.lock'); } } catch (e) { KazumiLogger().e('GStorage: Failed to delete box files for "$boxName"', error: e); } } static Future backupBox(String boxName, String backupFilePath) async { final appDocumentDir = await getApplicationSupportDirectory(); final hiveBoxFile = File('${appDocumentDir.path}/hive/$boxName.hive'); if (await hiveBoxFile.exists()) { await hiveBoxFile.copy(backupFilePath); print('Backup success: $backupFilePath'); } else { print('Hive box not exists'); } } static Future patchHistory(String backupFilePath) async { final backupFile = File(backupFilePath); final backupContent = await backupFile.readAsBytes(); final tempBox = await Hive.openBox('tempHistoryBox', bytes: backupContent); final tempBoxItems = tempBox.toMap().entries; for (var tempBoxItem in tempBoxItems) { if (histories.get(tempBoxItem.key) != null) { if (histories .get(tempBoxItem.key)! .lastWatchTime .isBefore(tempBoxItem.value.lastWatchTime)) { await histories.delete(tempBoxItem.key); await histories.put(tempBoxItem.key, tempBoxItem.value); } } else { await histories.put(tempBoxItem.key, tempBoxItem.value); } } await tempBox.close(); } static Future restoreCollectibles(String backupFilePath) async { final backupFile = File(backupFilePath); final backupContent = await backupFile.readAsBytes(); final tempBox = await Hive.openBox('tempCollectiblesBox', bytes: backupContent); final tempBoxItems = tempBox.toMap().entries; KazumiLogger().i( 'WebDav: restoring collectibles. tempCollectiblesBox length ${tempBoxItems.length}'); await collectibles.clear(); for (var tempBoxItem in tempBoxItems) { await collectibles.put(tempBoxItem.key, tempBoxItem.value); } await tempBox.close(); } static Future> getCollectiblesFromFile( String backupFilePath) async { final backupFile = File(backupFilePath); final backupContent = await backupFile.readAsBytes(); final tempBox = await Hive.openBox('tempCollectiblesBox', bytes: backupContent); final tempBoxItems = tempBox.toMap().entries; KazumiLogger().i( 'WebDav: get collectibles from file. tempCollectiblesBox length ${tempBoxItems.length}'); final List collectibles = []; for (var tempBoxItem in tempBoxItems) { collectibles.add(tempBoxItem.value); } await tempBox.close(); return collectibles; } static Future> getCollectChangesFromFile( String backupFilePath) async { final backupFile = File(backupFilePath); final backupContent = await backupFile.readAsBytes(); final tempBox = await Hive.openBox('tempCollectChangesBox', bytes: backupContent); final tempBoxItems = tempBox.toMap().entries; KazumiLogger().i( 'WebDav: get collectChanges from file. tempCollectChangesBox length ${tempBoxItems.length}'); final List collectChanges = []; for (var tempBoxItem in tempBoxItems) { collectChanges.add(tempBoxItem.value); } await tempBox.close(); return collectChanges; } static Future patchCollectibles( List remoteCollectibles, List remoteChanges) async { List localCollectibles = collectibles.values.toList(); List localChanges = collectChanges.values.toList(); final List newLocalChanges = localChanges.where((localChange) { return !remoteChanges .any((remoteChange) => remoteChange.id == localChange.id); }).toList(); newLocalChanges.sort((a, b) => a.timestamp.compareTo(b.timestamp)); // Process local changes for (var change in newLocalChanges) { // For delete action, we don't need to look up the local collectible. // We can directly remove the item from the remote list. if (change.action == 3) { // Action 3: delete remoteCollectibles .removeWhere((b) => b.bangumiItem.id == change.bangumiID); } else { // For add/update, we still need to look up the local collectible. final changedBangumiID = change.bangumiID.toString(); for (var localCollect in localCollectibles) { if (localCollect.bangumiItem.id.toString() == changedBangumiID) { if (change.action == 1) { // Action 1: add final exists = remoteCollectibles .any((b) => b.bangumiItem.id == localCollect.bangumiItem.id); if (!exists) { remoteCollectibles.add(localCollect); } else { final index = remoteCollectibles.indexWhere( (b) => b.bangumiItem.id == localCollect.bangumiItem.id); localCollect.type = change.type; if (index != -1) { // Update the entry with local data. remoteCollectibles[index] = localCollect; } } } else if (change.action == 2) { // Action 2: update final index = remoteCollectibles.indexWhere( (b) => b.bangumiItem.id == localCollect.bangumiItem.id); localCollect.type = change.type; if (index != -1) { // Update the entry with local data. remoteCollectibles[index] = localCollect; } } break; } } } } // merge local changes with remote changes final Map mergedMap = {}; for (var change in remoteChanges) { mergedMap[change.id] = change; } for (var change in newLocalChanges) { if (!mergedMap.containsKey(change.id)) { mergedMap[change.id] = change; } } final List mergedChanges = mergedMap.values.toList(); // Update local storage await collectibles.clear(); for (var collect in remoteCollectibles) { await collectibles.put(collect.bangumiItem.id, collect); } await collectChanges.clear(); for (var change in mergedChanges) { await collectChanges.put(change.id, change); } } // Prevent instantiation GStorage._(); } class SettingBoxKey { static const String hAenable = 'hAenable', hardwareDecoder = 'hardwareDecoder', searchEnhanceEnable = 'searchEnhanceEnable', autoUpdate = 'autoUpdate', alwaysOntop = 'alwaysOntop', defaultPlaySpeed = 'defaultPlaySpeed', defaultShortcutForwardPlaySpeed = 'defaultShortcutForwardPlaySpeed', defaultAspectRatioType = 'defaultAspectRatioType', buttonSkipTime = 'buttonSkipTime', arrowKeySkipTime = 'arrowKeySkipTime', danmakuEnhance = 'danmakuEnhance', danmakuBorder = 'danmakuBorder', danmakuBorderSize = 'danmakuBorderSize', danmakuOpacity = 'danmakuOpacity', danmakuFontSize = 'danmakuFontSize', danmakuTop = 'danmakuTop', danmakuScroll = 'danmakuScroll', danmakuBottom = 'danmakuBottom', danmakuMassive = 'danmakuMassive', danmakuDeduplication = 'danmakuDeduplication', danmakuArea = 'danmakuArea', danmakuColor = 'danmakuColor', danmakuDuration = 'danmakuDuration', danmakuLineHeight = 'danmakuLineHeight', danmakuEnabledByDefault = 'danmakuEnabledByDefault', danmakuBiliBiliSource = 'danmakuBiliBiliSource', danmakuGamerSource = 'danmakuGamerSource', danmakuDanDanSource = 'danmakuDanDanSource', danmakuFontWeight = 'danmakuFontWeight', danmakuFollowSpeed = 'danmakuFollowSpeed', themeMode = 'themeMode', themeColor = 'themeColor', privateMode = 'privateMode', autoPlay = 'autoPlay', autoPlayNext = 'autoPlayNext', playResume = 'playResume', showPlayerError = 'showPlayerError', oledEnhance = 'oledEnhance', displayMode = 'displayMode', enableGitProxy = 'enableGitProxy', enableSystemProxy = 'enableSystemProxy', defaultStartupPage = 'defaultStartupPage', /// Deprecated isWideScreen = 'isWideScreen', webDavEnable = 'webDavEnable', webDavEnableHistory = 'webDavEnableHistory', webDavEnableCollect = 'webDavEnableCollect', webDavURL = 'webDavURL', webDavUsername = 'webDavUsername', webDavPassword = 'webDavPasswd', lowMemoryMode = 'lowMemoryMode', showWindowButton = 'showWindowButton', useDynamicColor = 'useDynamicColor', exitBehavior = 'exitBehavior', playerDebugMode = 'playerDebugMode', syncPlayEndPoint = 'syncPlayEndPoint', androidEnableOpenSLES = 'androidEnableOpenSLES', androidVideoRenderer = 'androidVideoRenderer', defaultSuperResolutionType = 'defaultSuperResolutionType', superResolutionWarn = 'superResolutionWarn', playerDisableAnimations = 'playerDisableAnimations', playerLogLevel = 'playerLogLevel', searchNotShowWatchedBangumis = 'searchNotShowWatchedBangumis', searchNotShowAbandonedBangumis = 'searchNotShowAbandonedBangumis', timelineNotShowAbandonedBangumis = 'timelineNotShowAbandonedBangumis', timelineNotShowWatchedBangumis = 'timelineNotShowWatchedBangumis', useSystemFont = 'useSystemFont', forceAdBlocker = 'forceAdBlocker', proxyEnable = 'proxyEnable', proxyConfigured = 'proxyConfigured', proxyUrl = 'proxyUrl', proxyTestUrl = 'proxyTestUrl', showRating = 'showRating', downloadParallelEpisodes = 'downloadParallelEpisodes', downloadParallelSegments = 'downloadParallelSegments', downloadDanmaku = 'downloadDanmaku'; } ================================================ FILE: lib/utils/string_match.dart ================================================ // 计算两个字符串的编辑距离, 曾用于弹幕标题匹配 // 由于 DanDanPlay 现在直接提供基于 bgmBangumiID 的弹幕反查,此方法已不再使用 import 'dart:math'; int levenshteinDistance(String s1, String s2) { if (s1 == s2) return 0; if (s1.isEmpty) return s2.length; if (s2.isEmpty) return s1.length; List v0 = List.generate(s2.length + 1, (i) => i); List v1 = List.filled(s2.length + 1, 0); for (int i = 0; i < s1.length; i++) { v1[0] = i + 1; for (int j = 0; j < s2.length; j++) { int cost = (s1[i] == s2[j]) ? 0 : 1; v1[j + 1] = min(v1[j] + 1, min(v0[j + 1] + 1, v0[j] + cost)); } for (int j = 0; j < v0.length; j++) { v0[j] = v1[j]; } } return v1[s2.length]; } // 计算相似度百分比 double calculateSimilarity(String s1, String s2) { int maxLength = max(s1.length, s2.length); if (maxLength == 0) return 1.0; if (s1 == s2) return 1.0; return (1.0 - levenshteinDistance(s1, s2) / maxLength); } ================================================ FILE: lib/utils/syncplay.dart ================================================ // https://syncplay.pl/about/protocol/ import 'dart:async'; import 'dart:convert'; import 'dart:io'; const double PING_MOVING_AVERAGE_WEIGHT = 0.85; class SyncplayException implements Exception { final String message; SyncplayException(this.message); } class SyncplayConnectionException extends SyncplayException { SyncplayConnectionException(super.message); } class SyncplayProtocolException extends SyncplayException { SyncplayProtocolException(super.message); } abstract class SyncplayMessage { Map toJson(); } class HelloMessage extends SyncplayMessage { final String username; final String version; final String room; HelloMessage({ required this.username, required this.version, required this.room, }); @override Map toJson() => { 'Hello': { 'username': username, 'room': { 'name': room, }, 'version': version, 'features': { 'sharedPlaylists': true, 'chat': true, 'featureList': true, 'readiness': true, 'managedRooms': false, } }, }; } class StateMessage extends SyncplayMessage { final double position; final bool paused; final bool? doSeek; final String? setBy; // syncplay controll message final int? clientAck; final int? serverAck; // latency calculation double clientLatencyCalculation; double? latencyCalculation; final double clientRtt; StateMessage({ required this.position, required this.paused, this.setBy, this.doSeek, this.clientAck, this.serverAck, required this.clientLatencyCalculation, this.latencyCalculation, this.clientRtt = 0.0, }); @override Map toJson() => { 'State': { if (clientAck != null || serverAck != null) 'ignoringOnTheFly': { if (clientAck != null) 'client': clientAck, if (serverAck != null) 'server': serverAck, }, 'ping': { 'clientRtt': clientRtt, 'clientLatencyCalculation': clientLatencyCalculation, if (latencyCalculation != null) 'latencyCalculation': latencyCalculation, }, 'playstate': { 'position': position, 'paused': paused, if (setBy != null) 'setBy': setBy, 'doSeek': doSeek, }, }, }; } class SetMessage extends SyncplayMessage { final double? duration; final String? fileName; final String? username; final int? size; final String? setBy; final String? room; final bool? setJoined; final bool? setReady; SetMessage({ this.duration, this.fileName, this.username, this.size, this.setBy, this.room, this.setJoined, this.setReady, }); @override Map toJson() { if (setJoined != null && room != null && username != null) { return { "Set": { room: { "room": {"name": room}, "event": {"joined": true} }, } }; } if (setReady != null) { return { 'Set': { "ready": {"isReady": true, "manuallyInitiated": false} } }; } return { 'Set': { if (fileName != null) 'file': { 'duration': duration, 'name': fileName, 'size': size, }, if (room != null) "user": { setBy: { "room": {"name": room}, }, }, }, }; } } class ChatMessage extends SyncplayMessage { final String message; ChatMessage({ required this.message, }); @override Map toJson() => {'Chat': message}; } class TLSMessage extends SyncplayMessage { final String message; TLSMessage({ required this.message, }); @override Map toJson() => { 'TLS': { 'startTLS': message, }, }; } class SyncplayClient { final String _host; final int _port; bool _isTLS = false; Socket? _socket; String? _username; String? _currentRoom; String? _currentFileName; double _currentPositon = 0.0; bool _isPaused = true; StreamController>? _generalMessageController = StreamController.broadcast(); StreamController>? _roomMessageController = StreamController.broadcast(); StreamController>? _chatMessageController = StreamController.broadcast(); StreamController>? _flieChangedMessageController = StreamController.broadcast(); StreamController>? _positionChangedMessageController = StreamController.broadcast(); double? _lastLatencyCalculation; // Network status double _clientRtt = 0.0; double _serverRtt = 0.0; double _avrRtt = 0.0; double _fd = 0.0; // IgnoringOnTheFly int _clientIgnoringOnTheFly = 0; int _serverIgnoringOnTheFly = 0; bool get isConnected => _socket != null; bool get isTLS => _isTLS; String? get username => _username; String? get currentRoom => _currentRoom; String? get currentFileName => _currentFileName; double get clientRtt => _clientRtt; double get serverRtt => _serverRtt; double get avgRtt => _avrRtt; double get fd => _fd; Stream> get onGeneralMessage { _generalMessageController ??= StreamController.broadcast(); return _generalMessageController!.stream; } Stream> get onRoomMessage { _roomMessageController ??= StreamController.broadcast(); return _roomMessageController!.stream; } Stream> get onChatMessage { _chatMessageController ??= StreamController.broadcast(); return _chatMessageController!.stream; } Stream> get onFileChangedMessage { _flieChangedMessageController ??= StreamController.broadcast(); return _flieChangedMessageController!.stream; } Stream> get onPositionChangedMessage { _positionChangedMessageController ??= StreamController.broadcast(); return _positionChangedMessageController!.stream; } SyncplayClient({required String host, required int port}) : _host = host, _port = port; Future connect({bool enableTLS = true}) async { if (_generalMessageController?.isClosed ?? true) { _generalMessageController = StreamController.broadcast(); } if (_flieChangedMessageController?.isClosed ?? true) { _flieChangedMessageController = StreamController.broadcast(); } if (_positionChangedMessageController?.isClosed ?? true) { _positionChangedMessageController = StreamController.broadcast(); } try { await _socket?.close(); _socket = null; print('SyncPlay: connecting to Syncplay server: $_host:$_port'); _socket = await Socket.connect(_host, _port); print('SyncPlay: connected to Syncplay server: $_host:$_port'); _setupSocketHandlers(); if (enableTLS) { requestTLS(); } } on SocketException catch (e) { _generalMessageController?.addError( SyncplayConnectionException( 'SyncPlay: connection failed: ${e.message}'), ); } } Future requestTLS() async { print('SyncPlay: requesting TLS connection upgrade'); await _sendMessage(TLSMessage(message: 'send')); } Future joinRoom(String room, String username) async { print('SyncPlay: joining room: $room as $username'); await _sendMessage(HelloMessage( username: username, version: '1.7.0', room: room, )); } Future sendChatMessage(String message) async { if (_currentRoom == null || _username == null) { _generalMessageController?.addError( SyncplayProtocolException( 'SyncPlay: send chat message failed, not in a room'), ); return; } await _sendMessage(ChatMessage( message: message, )); } Future setSyncPlayPlaying( String bangumiName, double duration, int size) async { if (_currentRoom == null || _username == null) { _generalMessageController?.addError( SyncplayProtocolException( 'SyncPlay: set playing bangumi failed, not in a room'), ); return; } await _sendMessage(SetMessage( duration: duration, fileName: bangumiName, size: size, setBy: _username ?? '', room: _currentRoom ?? '')); } Future sendSyncPlaySyncRequest({bool? doSeek}) async { _sendState( position: _currentPositon, paused: _isPaused, doSeek: doSeek, stateChange: true, ); } Future disconnect() async { print('SyncPlay: disconnecting from Syncplay server: $_host:$_port'); await _generalMessageController?.close(); _generalMessageController = null; await _roomMessageController?.close(); _roomMessageController = null; await _chatMessageController?.close(); _chatMessageController = null; await _flieChangedMessageController?.close(); _flieChangedMessageController = null; await _positionChangedMessageController?.close(); _positionChangedMessageController = null; try { await _socket?.close(); } catch (_) {} _socket = null; _currentRoom = null; _username = null; _currentFileName = null; _currentPositon = 0.0; _isPaused = true; _isTLS = false; _lastLatencyCalculation = null; _clientIgnoringOnTheFly = 0; _serverIgnoringOnTheFly = 0; _clientRtt = 0.0; _serverRtt = 0.0; _avrRtt = 0.0; _fd = 0.0; } void setPosition(double position) { _currentPositon = position; } void setPaused(bool paused) { _isPaused = paused; } void _setupSocketHandlers() { String buffer = ''; _socket?.listen( (data) { final dataStr = utf8.decode(data); buffer += dataStr; while (true) { final startIndex = buffer.indexOf('{'); if (startIndex == -1) { break; } int braceCount = 0; int? endIndex; for (int i = startIndex; i < buffer.length; i++) { if (buffer[i] == '{') { braceCount++; } else if (buffer[i] == '}') { braceCount--; if (braceCount == 0) { endIndex = i; break; } } } if (endIndex == null) break; final jsonStr = buffer.substring(startIndex, endIndex + 1); try { // print( // 'SyncPlay: [${DateTime.now().millisecondsSinceEpoch / 1000.0}] received message: $jsonStr'); _handleMessage(json.decode(jsonStr)); } catch (e) { _generalMessageController?.addError( SyncplayProtocolException( 'SyncPlay: received data parse failed: $e'), ); } buffer = buffer.substring(endIndex + 1); } }, onError: (error) => _generalMessageController?.addError( SyncplayConnectionException('SyncPlay: socket error: $error'), ), onDone: () => _generalMessageController?.addError( SyncplayConnectionException('SyncPlay: connection closed'), ), ); } void _handleMessage(dynamic data) async { final json = data as Map; if (json.containsKey('TLS')) { if (json['TLS'].containsKey('startTLS')) { if (json['TLS']['startTLS'] == 'true') { var plainSocket = _socket; try { _socket = await SecureSocket.secure(plainSocket!); _setupSocketHandlers(); _isTLS = true; print('SyncPlay: TLS connection established'); try { plainSocket.close(); } catch (_) {} } catch (e) { print('SyncPlay: TLS connection upgrade failed: $e'); _socket = plainSocket; _isTLS = false; } } } return; } if (json.containsKey('Hello')) { if (json['Hello'].containsKey('room') && json['Hello']['room'].containsKey('name')) { _username = json['Hello']['username']; _currentRoom = json['Hello']['room']['name']; print( 'SyncPlay: joined room: $_currentRoom as $_username, version: ${json['Hello']['version']}'); _setReady(); } _generalMessageController?.add({ 'username': json['Hello']['username'], 'room': json['Hello']['room']['name'], }); return; } if (json.containsKey('State')) { if (json['State'].containsKey('ping')) { _lastLatencyCalculation = json['State']['ping']['latencyCalculation']?.toDouble(); if (json['State']['ping'].containsKey('serverRtt')) { _serverRtt = json['State']['ping']['serverRtt']?.toDouble() ?? 0.0; } _updateClientRttAndFd( json['State']["ping"]["clientLatencyCalculation"], _serverRtt); } if (json['State'].containsKey('ignoringOnTheFly')) { var ignoringOnTheFly = json['State']['ignoringOnTheFly']; if (ignoringOnTheFly.containsKey('server')) { _serverIgnoringOnTheFly = ignoringOnTheFly['server']; _clientIgnoringOnTheFly = 0; } else if (ignoringOnTheFly.containsKey('client')) { if (ignoringOnTheFly['client'] == _clientIgnoringOnTheFly) { _clientIgnoringOnTheFly = 0; } } } if (_clientIgnoringOnTheFly == 0) { _currentPositon = (json['State']['playstate']['paused'] ?? true) ? (json['State']['playstate']['position']?.toDouble() ?? 0.0) : ((json['State']['playstate']['position']?.toDouble() ?? 0.0) + _fd); _isPaused = json['State']['playstate']['paused'] ?? true; _positionChangedMessageController?.add({ 'calculatedPositon': (json['State']['playstate']['paused'] ?? true) ? (json['State']['playstate']['position']?.toDouble() ?? 0.0) : ((json['State']['playstate']['position']?.toDouble() ?? 0.0) + _fd), 'position': json['State']['playstate']['position']?.toDouble() ?? 0.0, 'paused': json['State']['playstate']['paused'] ?? true, 'doSeek': json['State']['playstate']['doSeek'] ?? false, 'setBy': json['State']['playstate']['setBy'] ?? '', 'clientRtt': _clientRtt, 'serverRtt': _serverRtt, 'avrRtt': _avrRtt, 'fd': _fd, }); } _sendState( position: _currentPositon, paused: _isPaused, ); return; } if (json.containsKey('Set')) { if (json['Set'].containsKey('playlistIndex')) { _roomMessageController?.add({ 'type': 'init', 'username': json['Set']['playlistIndex']['user'] ?? '', }); return; } if (json['Set'].containsKey('user')) { Map userMap = data['Set']['user']; userMap.forEach((username, details) { if (!details.containsKey('event')) { return; } var event = details['event'].keys.first ?? 'unknown'; _roomMessageController?.add({ 'type': event, 'username': username, }); }); for (var username in userMap.keys) { var userData = userMap[username]; if (userData is Map && userData.containsKey('file')) { var fileData = userData['file']; var fileName = fileData['name']; _currentFileName = fileName; _flieChangedMessageController?.add({ 'name': fileName, 'setBy': username, }); } } } return; } if (json.containsKey('Chat')) { if (json['Chat'].containsKey('message') && json['Chat'].containsKey('username')) { _chatMessageController?.add({ 'message': json['Chat']['message'], 'username': json['Chat']['username'], }); } return; } _generalMessageController?.addError( SyncplayProtocolException('SyncPlay: unknown message type'), ); } Future _setReady() async { if (_currentRoom == null || _username == null) { _generalMessageController?.addError( SyncplayProtocolException('SyncPlay: set ready failed, not in a room'), ); return; } await _sendMessage( SetMessage( setJoined: true, username: _username, room: _currentRoom, ), ); await _sendMessage( SetMessage( setReady: true, ), ); } Future _sendMessage(SyncplayMessage message) async { if (_socket == null) { _generalMessageController?.addError( SyncplayConnectionException('SyncPlay: not connected to server'), ); return; } final json = message.toJson(); final jsonStr = jsonEncode(json); // print( // 'SyncPlay: [${DateTime.now().millisecondsSinceEpoch / 1000.0}] sending message: $jsonStr'); _socket?.write('$jsonStr\r\n'); } void _sendState( {double? position, bool? paused, bool? doSeek, bool stateChange = false}) { int? clientArck; int? serverAck; if (stateChange) { _clientIgnoringOnTheFly = _clientIgnoringOnTheFly + 1; } if (_serverIgnoringOnTheFly > 0) { serverAck = _serverIgnoringOnTheFly; _serverIgnoringOnTheFly = 0; } if (_clientIgnoringOnTheFly > 0) { clientArck = _clientIgnoringOnTheFly; } _sendMessage(StateMessage( position: position ?? _currentPositon, paused: paused ?? _isPaused, latencyCalculation: _lastLatencyCalculation, clientLatencyCalculation: DateTime.now().millisecondsSinceEpoch / 1000.0, clientRtt: _clientRtt, setBy: _username, clientAck: clientArck, serverAck: serverAck, doSeek: doSeek, )); } void _updateClientRttAndFd(double? timestamp, double senderRtt) { if (timestamp == null) return; // Calculate RTT: current time minus the passed timestamp double newClientRtt = DateTime.now().millisecondsSinceEpoch / 1000.0 - timestamp; // If the new RTT is less than 0, it means the server is not responding if (newClientRtt < 0 || senderRtt < 0) return; _clientRtt = newClientRtt; // If it's the first time calculating, initialize the average RTT if (_avrRtt == 0) { _avrRtt = _clientRtt; } // Use moving average to update RTT, smooth the delay data _avrRtt = _avrRtt * PING_MOVING_AVERAGE_WEIGHT + _clientRtt * (1 - PING_MOVING_AVERAGE_WEIGHT); // Calculate the forward delay based on the sender's RTT if (senderRtt < _clientRtt) { _fd = _avrRtt / 2 + (_clientRtt - senderRtt); } else { _fd = _avrRtt / 2; } } } ================================================ FILE: lib/utils/syncplay_endpoint.dart ================================================ class SyncPlayEndPoint { final String host; final int port; const SyncPlayEndPoint({required this.host, required this.port}); } SyncPlayEndPoint? parseSyncPlayEndPoint(String endPoint) { final input = endPoint.trim(); if (input.isEmpty) { return null; } String host = ''; String portStr = ''; if (input.startsWith('[')) { final closeIndex = input.indexOf(']'); if (closeIndex == -1) { return null; } host = input.substring(1, closeIndex); final rest = input.substring(closeIndex + 1); if (!rest.startsWith(':')) { return null; } portStr = rest.substring(1); } else { final lastColonIndex = input.lastIndexOf(':'); if (lastColonIndex == -1) { return null; } host = input.substring(0, lastColonIndex); portStr = input.substring(lastColonIndex + 1); } host = host.trim(); portStr = portStr.trim(); if (host.isEmpty || portStr.isEmpty) { return null; } final port = int.tryParse(portStr); if (port == null || port <= 0 || port > 65535) { return null; } return SyncPlayEndPoint(host: host, port: port); } ================================================ FILE: lib/utils/timed_shutdown_service.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; import 'package:kazumi/utils/logger.dart'; class TimedShutdownService { static final TimedShutdownService _instance = TimedShutdownService._internal(); factory TimedShutdownService() => _instance; TimedShutdownService._internal(); Timer? _shutdownTimer; int _remainingSeconds = 0; bool _isDialogShowing = false; /// Last set minutes, used for repeat functionality int _lastSetMinutes = 0; /// Callback to invoke when timer expires (e.g., pause video) VoidCallback? _onExpiredCallback; /// Remaining time in seconds notifier final ValueNotifier remainingSecondsNotifier = ValueNotifier(0); /// Currently set minutes notifier (for UI display) final ValueNotifier setMinutesNotifier = ValueNotifier(0); /// Whether a shutdown timer is currently active bool get isActive => _shutdownTimer != null && _shutdownTimer!.isActive; /// Currently set minutes (0 = disabled) int get setMinutes => setMinutesNotifier.value; /// Remaining time in seconds int get remainingSeconds => remainingSecondsNotifier.value; /// Start the shutdown timer with the given duration in minutes /// [onExpired] callback is invoked when timer expires (before showing dialog) void start(int minutes, {VoidCallback? onExpired}) { cancel(); if (minutes <= 0) return; _lastSetMinutes = minutes; _remainingSeconds = minutes * 60; remainingSecondsNotifier.value = _remainingSeconds; setMinutesNotifier.value = minutes; _onExpiredCallback = onExpired; // Update remaining time every second (runs globally, not tied to playback) _shutdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_remainingSeconds > 0) { _remainingSeconds--; remainingSecondsNotifier.value = _remainingSeconds; } if (_remainingSeconds <= 0) { timer.cancel(); _shutdownTimer = null; _onTimerExpired(); } }); } /// Repeat the timer with the last set duration void repeat() { if (_lastSetMinutes > 0) { start(_lastSetMinutes, onExpired: _onExpiredCallback); } } /// Cancel the current shutdown timer void cancel() { _shutdownTimer?.cancel(); _shutdownTimer = null; _remainingSeconds = 0; _onExpiredCallback = null; if (remainingSecondsNotifier.value != 0) { remainingSecondsNotifier.value = 0; } if (setMinutesNotifier.value != 0) { setMinutesNotifier.value = 0; } // If dialog is showing, dismiss it if (_isDialogShowing) { KazumiDialog.dismiss(); _isDialogShowing = false; } } /// Called when timer expires: invoke callback and show dialog void _onTimerExpired() { // Reset UI state so it doesn't show 00:00 setMinutesNotifier.value = 0; // Invoke the callback if set (e.g., pause video) try { _onExpiredCallback?.call(); } catch (e) { KazumiLogger().e('TimedShutdownService: onExpired callback failed', error: e); } _showTimerExpiredDialog(); } /// Show the timer expired dialog with repeat/close options void _showTimerExpiredDialog() { if (_isDialogShowing) return; _isDialogShowing = true; KazumiDialog.show( clickMaskDismiss: false, onDismiss: () { _isDialogShowing = false; }, builder: (context) { return AlertDialog( title: const Text('定时关闭'), content: const Text('定时时间已到,视频已暂停'), actions: [ TextButton( onPressed: () { _isDialogShowing = false; KazumiDialog.dismiss(); repeat(); KazumiDialog.showToast(message: '已重新开始 $_lastSetMinutes 分钟定时'); }, child: const Text('重复'), ), TextButton( onPressed: () { _isDialogShowing = false; KazumiDialog.dismiss(); }, child: Text( '关闭', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), ], ); }, ); } /// Format remaining seconds to a readable string (e.g., "15:30") String formatRemainingTime() { int totalSeconds = remainingSecondsNotifier.value; if (totalSeconds <= 0) return '00:00'; final minutes = totalSeconds ~/ 60; final seconds = totalSeconds % 60; return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } /// Format minutes to readable display string (e.g., "1 小时 30 分钟") String formatMinutesToDisplay(int totalMinutes) { final hours = totalMinutes ~/ 60; final minutes = totalMinutes % 60; if (hours > 0 && minutes > 0) { return '$hours 小时 $minutes 分钟'; } else if (hours > 0) { return '$hours 小时'; } else { return '$minutes 分钟'; } } /// Show custom timer picker dialog and start timer if user confirms /// Uses KazumiDialog to avoid context-related resource leaks /// [onExpired] callback is invoked when timer expires (before showing dialog) static void showCustomTimerDialog({ String title = '自定义定时', bool autoStart = true, VoidCallback? onExpired, void Function(int)? onResult, }) { int selectedHours = 0; int selectedMinutes = 0; KazumiDialog.show( builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: Text(title), content: SizedBox( height: 200, child: Row( children: [ // Hours picker Expanded( child: Column( children: [ const Text('时', style: TextStyle(fontSize: 14)), const SizedBox(height: 8), Expanded( child: CupertinoPicker( scrollController: FixedExtentScrollController(initialItem: selectedHours), itemExtent: 40, onSelectedItemChanged: (index) { setState(() => selectedHours = index); }, children: List.generate(25, (index) => Center( child: Text(index.toString().padLeft(2, '0'), style: const TextStyle(fontSize: 20)), )), ), ), ], ), ), const Text(':', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), // Minutes picker Expanded( child: Column( children: [ const Text('分', style: TextStyle(fontSize: 14)), const SizedBox(height: 8), Expanded( child: CupertinoPicker( scrollController: FixedExtentScrollController(initialItem: selectedMinutes), itemExtent: 40, onSelectedItemChanged: (index) { setState(() => selectedMinutes = index); }, children: List.generate(60, (index) => Center( child: Text(index.toString().padLeft(2, '0'), style: const TextStyle(fontSize: 20)), )), ), ), ], ), ), ], ), ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { final totalMinutes = selectedHours * 60 + selectedMinutes; if (totalMinutes <= 0) { KazumiDialog.showToast(message: '请选择有效的时间'); return; } KazumiDialog.dismiss(); if (autoStart) { TimedShutdownService().start(totalMinutes, onExpired: onExpired); KazumiDialog.showToast( message: '已设置 ${TimedShutdownService().formatMinutesToDisplay(totalMinutes)} 后定时关闭', ); } onResult?.call(totalMinutes); }, child: const Text('确定'), ), ], ); }, ); }, ); } } ================================================ FILE: lib/utils/utils.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:kazumi/modules/danmaku/danmaku_module.dart'; import 'package:kazumi/request/api.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/mortis.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; class Utils { static final Random random = Random(); static bool? _isDocumentStartScriptSupported; /// 检查 Android WebView 是否支持 DOCUMENT_START_SCRIPT 特性 static Future checkWebViewFeatureSupport() async { if (Platform.isAndroid) { _isDocumentStartScriptSupported = await PlatformWebViewFeature.static() .isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT); } } static bool get isDocumentStartScriptSupported => _isDocumentStartScriptSupported ?? false; static Future isLowResolution() async { if (Platform.isMacOS) { return false; } Map screenInfo = await getScreenInfo(); if (screenInfo['height']! / screenInfo['ratio']! < 900) { return true; } return false; } static String getRandomUA() { final random = Random(); String randomElement = userAgentsList[random.nextInt(userAgentsList.length)]; return randomElement; } static String getRandomAcceptedLanguage() { final random = Random(); String randomElement = acceptLanguageList[random.nextInt(acceptLanguageList.length)]; return randomElement; } static Future> getScreenInfo() async { final MediaQueryData mediaQuery = MediaQueryData.fromView( WidgetsBinding.instance.platformDispatcher.views.first); final Size screenSize = WidgetsBinding.instance.platformDispatcher.displays.first.size; final double screenRatio = mediaQuery.devicePixelRatio; Map? screenInfo = {}; screenInfo = { 'width': screenSize.width, 'height': screenSize.height, 'ratio': screenRatio }; return screenInfo; } // 从URL参数中解析 m3u8/mp4 static String decodeVideoSource(String iframeUrl) { var decodedUrl = Uri.decodeFull(iframeUrl); RegExp regExp = RegExp(r'(http[s]?://.*?\.m3u8)|(http[s]?://.*?\.mp4)', caseSensitive: false); Uri uri = Uri.parse(decodedUrl); Map params = uri.queryParameters; String matchedUrl = iframeUrl; params.forEach((key, value) { if (regExp.hasMatch(value)) { matchedUrl = value; return; } }); return Uri.encodeFull(matchedUrl); } // 完全相对时间显示 static String formatTimestampToRelativeTime(timeStamp) { var difference = DateTime.now() .difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000)); if (difference.inDays > 365) { return '${difference.inDays ~/ 365}年前'; } else if (difference.inDays > 30) { return '${difference.inDays ~/ 30}个月前'; } else if (difference.inDays > 0) { return '${difference.inDays}天前'; } else if (difference.inHours > 0) { return '${difference.inHours}小时前'; } else if (difference.inMinutes > 0) { return '${difference.inMinutes}分钟前'; } else { return '刚刚'; } } // 时间显示,刚刚,x分钟前 static String dateFormat(timeStamp, {formatType = 'list'}) { // 当前时间 int time = (DateTime.now().millisecondsSinceEpoch / 1000).round(); // 对比 int distance = (time - timeStamp).toInt(); // 当前年日期 String currentYearStr = 'MM月DD日 hh:mm'; String lastYearStr = 'YY年MM月DD日 hh:mm'; if (formatType == 'detail') { currentYearStr = 'MM-DD hh:mm'; lastYearStr = 'YY-MM-DD hh:mm'; return CustomStamp_str( timestamp: timeStamp, date: lastYearStr, toInt: false, formatType: formatType); } if (distance <= 60) { return '刚刚'; } else if (distance <= 3600) { return '${(distance / 60).floor()}分钟前'; } else if (distance <= 43200) { return '${(distance / 60 / 60).floor()}小时前'; } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year == DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) { return CustomStamp_str( timestamp: timeStamp, date: currentYearStr, toInt: false, formatType: formatType); } else { return CustomStamp_str( timestamp: timeStamp, date: lastYearStr, toInt: false, formatType: formatType); } } // 时间戳转时间 static String CustomStamp_str( {int? timestamp, // 为空则显示当前时间 String? date, // 显示格式,比如:'YY年MM月DD日 hh:mm:ss' bool toInt = true, // 去除0开头 String? formatType}) { timestamp ??= (DateTime.now().millisecondsSinceEpoch / 1000).round(); String timeStr = (DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)).toString(); dynamic dateArr = timeStr.split(' ')[0]; dynamic timeArr = timeStr.split(' ')[1]; String YY = dateArr.split('-')[0]; String MM = dateArr.split('-')[1]; String DD = dateArr.split('-')[2]; String hh = timeArr.split(':')[0]; String mm = timeArr.split(':')[1]; String ss = timeArr.split(':')[2]; ss = ss.split('.')[0]; // 去除0开头 if (toInt) { MM = (int.parse(MM)).toString(); DD = (int.parse(DD)).toString(); hh = (int.parse(hh)).toString(); mm = (int.parse(mm)).toString(); } if (date == null) { return timeStr; } // if (formatType == 'list' && int.parse(DD) > DateTime.now().day - 2) { // return '昨天'; // } date = date .replaceAll('YY', YY) .replaceAll('MM', MM) .replaceAll('DD', DD) .replaceAll('hh', hh) .replaceAll('mm', mm) .replaceAll('ss', ss); if (int.parse(YY) == DateTime.now().year && int.parse(MM) == DateTime.now().month) { // 当天 if (int.parse(DD) == DateTime.now().day) { return '今天'; } } return date; } static String makeHeroTag(v) { return v.toString() + random.nextInt(9999).toString(); } // 版本对比 static bool needUpdate(localVersion, remoteVersion) { List localVersionList = localVersion.split('.'); List remoteVersionList = remoteVersion.split('.'); for (int i = 0; i < localVersionList.length; i++) { int localVersion = int.parse(localVersionList[i]); int remoteVersion = int.parse(remoteVersionList[i]); if (remoteVersion > localVersion) { return true; } else if (remoteVersion < localVersion) { return false; } } return false; } // 日期字符串转换为 weekday (eg: 2024-09-23 -> 1 (星期一)) static int dateStringToWeekday(String dateString) { try { DateTime date = DateTime.parse(dateString); return date.weekday; } catch (_) { return 1; } } static String jsonToKazumiBase64(String jsonStr) { String base64Str = base64Encode(utf8.encode(jsonStr)); return 'kazumi://$base64Str'; } static String kazumiBase64ToJson(String kazumiBase64Str) { if (!kazumiBase64Str.startsWith('kazumi://')) { return ''; } String base64Str = kazumiBase64Str.substring(9); String jsonStr = utf8.decode(base64.decode(base64Str)); return jsonStr; } static String durationToString(Duration duration) { String pad(int n) => n.toString().padLeft(2, '0'); var hours = pad(duration.inHours % 24); var minutes = pad(duration.inMinutes % 60); var seconds = pad(duration.inSeconds % 60); if (hours == "00") { return "$minutes:$seconds"; } else { return "$hours:$minutes:$seconds"; } } static Future latest() async { try { var resp = await Dio().get>(Api.latestApp); if (resp.data?.containsKey("tag_name") ?? false) { return resp.data!["tag_name"]; } else { throw resp.data?["message"]; } } catch (e) { return Api.version; } } static oledDarkTheme(ThemeData defaultDarkTheme) { return defaultDarkTheme.copyWith( scaffoldBackgroundColor: Colors.black, colorScheme: defaultDarkTheme.colorScheme.copyWith( onPrimary: Colors.black, onSecondary: Colors.black, // background: Colors.black, // onBackground: Colors.black, surface: Colors.black, onSurface: Colors.white, ), ); } static generateDanmakuColor(int colorValue) { // 提取颜色分量 int red = (colorValue >> 16) & 0xFF; int green = (colorValue >> 8) & 0xFF; int blue = colorValue & 0xFF; // 创建Color对象 Color color = Color.fromARGB(255, red, green, blue); return color; } static List mergeDuplicateDanmakus( List danmakus, { double timeWindowSeconds = 0, }) { final Map> grouped = {}; // 弹幕规范化处理 // 去首尾空格并小写,全角转半角,去掉所有空白、标点符号,连续重复内容压缩,保留日语字符 for (var d in danmakus) { String text = d.message; text = text.trim().toLowerCase(); final buffer = StringBuffer(); for (int i = 0; i < text.length; i++) { int code = text.codeUnitAt(i); if (code == 0x3000) { buffer.writeCharCode(0x20); } else if (code >= 0xFF01 && code <= 0xFF5E) { buffer.writeCharCode(code - 0xFEE0); } else { buffer.writeCharCode(code); } } text = buffer.toString(); text = text.replaceAll(RegExp(r'\s+'), ''); text = text.replaceAll(RegExp( r'[^\w\u4e00-\u9fff\u3040-\u309F\u30A0-\u30FF\u31F0-\u31FF\uFF65-\uFF9F]', unicode: true, ),''); text = text.replaceAllMapped(RegExp(r'(.)\1{2,}'), (match) { final char = match.group(1)!; return char * 3; }); grouped.putIfAbsent(text, () => []); grouped[text]!.add(d); } final List result = []; grouped.forEach((normalized, list) { if (list.isEmpty) return; if (timeWindowSeconds <= 0) { if (list.length == 1) { result.add(list.first); } else { result.add( Danmaku( message: '${list.first.message} x${list.length}', time: list.first.time, // 默认取第一条 type: 5, color: Utils.generateDanmakuColor(0xFFFFFF), // 白色弹幕 source: list.first.source, ), ); } return; } list.sort((a, b) => a.time.compareTo(b.time)); List currentGroup = []; for (var item in list) { if (currentGroup.isEmpty) { currentGroup.add(item); continue; } final last = currentGroup.last; if ((item.time - last.time) <= timeWindowSeconds) { currentGroup.add(item); } else { if (currentGroup.length == 1) { result.add(currentGroup.first); } else { result.add( Danmaku( message: '${currentGroup.first.message} x${currentGroup.length}', time: currentGroup.first.time, type: 5, color: Utils.generateDanmakuColor(0xFFFFFF), source: currentGroup.first.source, ), ); } currentGroup = [item]; } } if (currentGroup.isNotEmpty) { if (currentGroup.length == 1) { result.add(currentGroup.first); } else { result.add( Danmaku( message: '${currentGroup.first.message} x${currentGroup.length}', time: currentGroup.first.time, type: 5, color: Utils.generateDanmakuColor(0xFFFFFF), source: currentGroup.first.source, ), ); } } }); return result; } static int extractEpisodeNumber(String input) { RegExp regExp = RegExp(r'第?(\d+)[话集]?'); Match? match = regExp.firstMatch(input); if (match != null && match.group(1) != null) { return int.tryParse(match.group(1)!) ?? 0; } return 0; } /// 判断是否为桌面设备 static bool isDesktop() { return Platform.isWindows || Platform.isMacOS || Platform.isLinux; } /// 判断设备是否为宽屏 static bool isWideScreen() { final MediaQueryData mediaQuery = MediaQueryData.fromView( WidgetsBinding.instance.platformDispatcher.views.first); final bool isWideScreen = mediaQuery.size.shortestSide >= 600 && mediaQuery.size.shortestSide / mediaQuery.size.longestSide >= 9 / 16; return isWideScreen; } /// 判断设备是否为平板 static bool isTablet() { return isWideScreen() && !isDesktop(); } /// 判断设备是否需要紧凑布局 static bool isCompact() { return !isDesktop() && !isWideScreen(); } /// 判断是否分屏模式 (android only) static Future isInMultiWindowMode() async { if (Platform.isAndroid) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { final bool result = await platform.invokeMethod('checkIfInMultiWindowMode'); return result; } on PlatformException catch (e) { print("Failed to check multi window mode: '${e.message}'."); return false; } } return false; } /// 判定是否运行在X11环境下 (Linux only) static Future isRunningOnX11() async { if (Platform.isLinux) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { final bool result = await platform.invokeMethod('isRunningOnX11'); return result; } on PlatformException catch (e) { print("Failed to check X11 environment: '${e.message}'."); return false; } } return false; } // Deprecated static Future enterWindowsFullscreen() async { if (Platform.isWindows) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { await platform.invokeMethod('enterFullscreen'); } on PlatformException catch (e) { print("Failed to enter native window mode: '${e.message}'."); } } } // Deprecated static Future exitWindowsFullscreen() async { if (Platform.isWindows) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { await platform.invokeMethod('exitFullscreen'); } on PlatformException catch (e) { print("Failed to exit native window mode: '${e.message}'."); } } } // 进入全屏显示 static Future enterFullScreen({bool lockOrientation = true}) async { // if (Platform.isWindows) { // await enterWindowsFullscreen(); // return; // } if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { await windowManager.setFullScreen(true); return; } await SystemChrome.setEnabledSystemUIMode( SystemUiMode.immersiveSticky, ); if (!lockOrientation) { return; } if (Platform.isAndroid) { bool isInMultiWindowMode = await Utils.isInMultiWindowMode(); if (isInMultiWindowMode) { return; } } await landScape(); } static Future getAndroidSdkVersion() async { if (Platform.isAndroid) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { final int sdkVersion = await platform.invokeMethod('getAndroidSdkVersion'); return sdkVersion; } on PlatformException catch (e) { KazumiLogger().e("Failed to get Android SDK version: '${e.message}'."); return 0; } } return 0; } //退出全屏显示 static Future exitFullScreen({bool lockOrientation = true}) async { // if (Platform.isWindows) { // await exitWindowsFullscreen(); // } if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { await windowManager.setFullScreen(false); } late SystemUiMode mode = SystemUiMode.edgeToEdge; try { if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid) { const platform = MethodChannel('com.predidit.kazumi/intent'); try { final int sdkVersion = await platform.invokeMethod('getAndroidSdkVersion'); if (sdkVersion < 29) { mode = SystemUiMode.manual; } } on PlatformException catch (e) { KazumiLogger() .e("Failed to get Android SDK version: '${e.message}'."); } } await SystemChrome.setEnabledSystemUIMode( mode, overlays: SystemUiOverlay.values, ); if (Utils.isCompact() && lockOrientation) { if (Platform.isAndroid) { bool isInMultiWindowMode = await Utils.isInMultiWindowMode(); if (isInMultiWindowMode) { return; } } verticalScreen(); } } } catch (exception, stacktrace) { KazumiLogger().e('DisPlay: failed to exit full screen', error: exception, stackTrace: stacktrace); } } //横屏 static Future landScape() async { dynamic document; try { if (kIsWeb) { await document.documentElement?.requestFullscreen(); } else if (Platform.isAndroid || Platform.isIOS) { await SystemChrome.setPreferredOrientations( [ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ], ); } } catch (exception, stacktrace) { KazumiLogger().e('Display: failed to enter landscape mode', error: exception, stackTrace: stacktrace); } } //竖屏 static Future verticalScreen() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); } // 解除屏幕旋转限制 static Future unlockScreenRotation() async { await SystemChrome.setPreferredOrientations([]); } static String getSeasonStringByMonth(int month) { if (month <= 3) return '冬'; if (month <= 6) return '春'; if (month <= 9) return '夏'; return '秋'; } // 进入桌面设备小窗模式 static Future enterDesktopPIPWindow() async { await windowManager.setAlwaysOnTop(true); await windowManager.setSize(const Size(480, 270)); } // 退出桌面设备小窗模式 static Future exitDesktopPIPWindow() async { bool isLowResolution = await Utils.isLowResolution(); await windowManager.setAlwaysOnTop(false); await windowManager.setSize( isLowResolution ? const Size(800, 600) : const Size(1280, 860)); await windowManager.center(); } static bool isSameSeason(DateTime d1, DateTime d2) { return d1.year == d2.year && (d1.month - d2.month).abs() <= 2; } static Future getPlayerTempPath() async { final directory = await getTemporaryDirectory(); return directory.path; } static String buildShadersAbsolutePath( String baseDirectory, List shaders) { List absolutePaths = shaders.map((shader) { return path.join(baseDirectory, shader); }).toList(); if (Platform.isWindows) { return absolutePaths.join(';'); } return absolutePaths.join(':'); } static String generateDandanSignature(String path, int timestamp) { String id = mortis['id']!; String value = mortis['value']!; String data = id + timestamp.toString() + path + value; var bytes = utf8.encode(data); var digest = sha256.convert(bytes); return base64Encode(digest.bytes); } /// 格式化日期 /// eg: 2025-07-27T09:14:12Z -> 2025-07-27 static String formatDate(String dateString) { try { final date = DateTime.parse(dateString); return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } catch (e) { return dateString; } } /// 计算文件的 SHA256 哈希值 static Future calculateFileHash(File file) async { final bytes = await file.readAsBytes(); final digest = sha256.convert(bytes); return digest.toString(); } /// 销毁播放器菜单 static Future disposePlayerMenu() async { if (!Platform.isMacOS) return; //暂时只适配macOS const MethodChannel appmenu = MethodChannel("com.predidit.kazumi/appmenu"); await appmenu.invokeMethod("setMenuEnabled", { "menu": "PlayerMenu", "enable": false, }); } /// 初始化播放器菜单 static Future initPlayerMenu( Map actions) async { if (!Platform.isMacOS) return; //暂时只适配macOS const MethodChannel appmenu = MethodChannel("com.predidit.kazumi/appmenu"); await appmenu.invokeMethod("setMenuEnabled", { "menu": "PlayerMenu", "enable": true, }); appmenu.setMethodCallHandler((call) async { final action = actions[call.method]; action?.call(); }); } } ================================================ FILE: lib/utils/webdav.dart ================================================ import 'dart:io'; import 'package:webdav_client/webdav_client.dart' as webdav; import 'package:hive_ce/hive.dart'; import 'package:path_provider/path_provider.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/modules/collect/collect_module.dart'; import 'package:kazumi/modules/collect/collect_change_module.dart'; class WebDav { late String webDavURL; late String webDavUsername; late String webDavPassword; late Directory webDavLocalTempDirectory; late webdav.Client client; bool initialized = false; // make sure only one upload history task at a time bool isHistorySyncing = false; WebDav._internal(); static final WebDav _instance = WebDav._internal(); factory WebDav() => _instance; Future init() async { var directory = await getApplicationSupportDirectory(); webDavLocalTempDirectory = Directory('${directory.path}/webdavTemp'); Box setting = GStorage.setting; webDavURL = setting.get(SettingBoxKey.webDavURL, defaultValue: ''); webDavUsername = setting.get(SettingBoxKey.webDavUsername, defaultValue: ''); webDavPassword = setting.get(SettingBoxKey.webDavPassword, defaultValue: ''); if (webDavURL.isEmpty) { //KazumiLogger().log(Level.warning, 'WebDAV URL is not set'); throw Exception('请先填写WebDAV URL'); } client = webdav.newClient( webDavURL, user: webDavUsername, password: webDavPassword, debug: false, ); client.setHeaders({'accept-charset': 'utf-8'}); try { await client.ping(); try { // KazumiLogger().log(Level.warning, 'webDav backup directory not exists, creating'); await client.mkdir('/kazumiSync'); if (!await webDavLocalTempDirectory.exists()) { await webDavLocalTempDirectory.create(recursive: true); } initialized = true; KazumiLogger().i('WebDav: webDav backup directory create success'); } catch (_) { KazumiLogger().e('WebDav: webDav backup directory create failed'); rethrow; } } catch (e) { KazumiLogger().e('WebDav: WebDAV ping failed', error: e); rethrow; } } Future update(String boxName) async { var directory = await getApplicationSupportDirectory(); final localFilePath = '${directory.path}/hive/$boxName.hive'; final tempFilePath = '${webDavLocalTempDirectory.path}/$boxName.tmp'; final webDavPath = '/kazumiSync/$boxName.tmp'; await File(localFilePath) .copy(tempFilePath); try { await client.remove('$webDavPath.cache'); } catch (_) {} await client.writeFromFile(tempFilePath, '$webDavPath.cache', onProgress: (c, t) { // print(c / t); }); try { await client.remove(webDavPath); } catch (_) { KazumiLogger().w('WebDav: former backup file not exist'); } await client.rename( '$webDavPath.cache', webDavPath, true); try { await File(tempFilePath).delete(); } catch (_) {} } Future updateHistory() async { if (isHistorySyncing) { KazumiLogger().w('WebDav: History is currently syncing'); throw Exception('History is currently syncing'); } isHistorySyncing = true; try { await update('histories'); } catch (e) { KazumiLogger().e('WebDav: update history failed', error: e); rethrow; } finally { isHistorySyncing = false; } } Future updateCollectibles() async { // don't try muliti thread update here // some webdav server may not support muliti thread write // you will get 423 locked error try { await update('collectibles'); if (GStorage.collectChanges.isNotEmpty) { await update('collectchanges'); } } catch (e) { KazumiLogger().e('WebDav: update collectibles failed', error: e); rethrow; } } Future download(String boxName) async { String fileName = '$boxName.tmp'; final existingFile = File('${webDavLocalTempDirectory.path}/$fileName'); if (await existingFile.exists()) { await existingFile.delete(); } await client.read2File('/kazumiSync/$fileName', existingFile.path, onProgress: (c, t) { // print(c / t); }); } Future downloadAndPatchHistory() async { if (isHistorySyncing) { KazumiLogger().w('WebDav: History is currently syncing'); throw Exception('History is currently syncing'); } isHistorySyncing = true; String fileName = 'histories.tmp'; try { final existingFile = File('${webDavLocalTempDirectory.path}/$fileName'); await download('histories'); await GStorage.patchHistory(existingFile.path); } catch (e) { KazumiLogger() .e('WebDav: download and patch history failed', error: e); rethrow; } finally { isHistorySyncing = false; } } Future syncCollectibles() async { List remoteCollectibles = []; List remoteChanges = []; final files = await client.readDir('/kazumiSync'); final collectiblesExists = files.any((file) => file.name == 'collectibles.tmp'); final changesExists = files.any((file) => file.name == 'collectchanges.tmp'); if (!collectiblesExists && !changesExists) { await updateCollectibles(); return; } List> downloadFutures = []; if (collectiblesExists) { downloadFutures.add(download('collectibles').catchError((e) { KazumiLogger().e('WebDav: download collectibles failed', error: e); throw Exception('WebDav: download collectibles failed'); })); } if (changesExists) { downloadFutures.add(download('collectchanges').catchError((e) { KazumiLogger().e('WebDav: download collectchanges failed', error: e); throw Exception('WebDav: download collectchanges failed'); })); } if (downloadFutures.isNotEmpty) { await Future.wait(downloadFutures); } try { if (collectiblesExists) { remoteCollectibles = await GStorage.getCollectiblesFromFile( '${webDavLocalTempDirectory.path}/collectibles.tmp'); } if (changesExists) { remoteChanges = await GStorage.getCollectChangesFromFile( '${webDavLocalTempDirectory.path}/collectchanges.tmp'); } } catch (e) { KazumiLogger().e('WebDav: get collectibles failed', error: e); throw Exception('WebDav: get collectibles from file failed'); } if (remoteChanges.isNotEmpty || remoteCollectibles.isNotEmpty) { await GStorage.patchCollectibles(remoteCollectibles, remoteChanges); } await updateCollectibles(); } Future ping() async { try { await client.ping(); } catch (e) { KazumiLogger().e('WebDav: WebDav ping failed', error: e); rethrow; } } } ================================================ FILE: lib/webview/captcha/captcha_webview_controller.dart ================================================ import 'dart:io'; import 'dart:async'; import 'package:kazumi/webview/captcha/impl/captcha_webview_inappwebview_impl.dart'; import 'package:kazumi/webview/captcha/impl/captcha_webview_windows_impl.dart'; import 'package:kazumi/webview/captcha/impl/captcha_webview_linux_impl.dart'; abstract class CaptchaWebviewController { /// Webview controller T? webviewController; /// For type-1 (captcha image), whether a captcha image has been detected, /// used to determine if verification is successful after page navigation bool captchaWasFound = false; /// For type-2 (auto-click button), we set this flag when the button click is triggered. /// Then on page navigation or DOM change, if this flag is set, /// we can confirm verification success without relying solely on captcha disappearance. bool buttonWasClicked = false; final StreamController captchaImageFoundController = StreamController.broadcast(); final StreamController captchaDisappearedController = StreamController.broadcast(); final StreamController initEventController = StreamController.broadcast(); final StreamController logEventController = StreamController.broadcast(); /// WebView 初始化完成事件 Stream get onInitialized => initEventController.stream; /// 验证码图片 src 找到时触发(携带图片绝对 URL) Stream get onCaptchaImageFound => captchaImageFoundController.stream; /// 验证码图片从页面消失时触发 Stream get onCaptchaDisappeared => captchaDisappearedController.stream; /// 调试日志 Stream get onLog => logEventController.stream; /// 初始化 WebView Future init(); /// 加载指定 URL,并注入监听验证码图片的 JS 脚本(类型1:图片验证码) /// /// [url] 要加载的页面地址(一般为搜索 URL) /// [captchaXpath] 验证码图片元素的 XPath 选择器 /// [inputXpath] 可选,验证码输入框的 XPath。如果提供,会在检测验证码前先触发输入框的 focus 事件(某些站点需要) Future loadPage(String url, String captchaXpath, {String? inputXpath}); /// 加载指定 URL,并注入监听验证按钮的 JS 脚本(类型2:自动点击验证按钮) /// /// 检测到 [buttonXpath] 元素后立即模拟点击;按钮消失时触发 [onCaptchaDisappeared]。 /// [url] 要加载的页面地址 /// [buttonXpath] 验证按钮元素的 XPath 选择器 Future loadPageForButtonClick(String url, String buttonXpath); /// 在 WebView 内通过 JS 模拟输入验证码并模拟点击提交按钮 /// /// [captchaCode] 用户输入的验证码文本 /// [inputXpath] 验证码输入框元素的 XPath /// [buttonXpath] 提交按钮元素的 XPath Future submitCaptchaInteract( String captchaCode, String inputXpath, String buttonXpath); /// 获取当前页面的 Cookie 字符串("key1=val1; key2=val2") /// /// [pageUrl] 当前加载的页面地址,部分平台用于精确过滤 Cookie Future getCookieString(String pageUrl); /// 卸载当前页面(跳转到 about:blank) Future unloadPage(); /// 释放 WebView 资源 void dispose(); } class CaptchaWebviewControllerFactory { static CaptchaWebviewController getController() { if (Platform.isWindows) { return CaptchaWebviewWindowsImpl(); } if (Platform.isLinux) { return CaptchaWebviewLinuxImpl(); } // Android, iOS, macOS return CaptchaWebviewInAppWebviewImpl(); } } ================================================ FILE: lib/webview/captcha/impl/captcha_webview_inappwebview_impl.dart ================================================ import 'dart:async'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/webview/captcha/captcha_webview_controller.dart'; import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; class CaptchaWebviewInAppWebviewImpl extends CaptchaWebviewController { PlatformHeadlessInAppWebView? _headlessWebView; bool _handlersRegistered = false; String _currentCaptchaImageXpath = ''; String _currentInputXpath = ''; @override Future init() async { _headlessWebView ??= PlatformHeadlessInAppWebView( PlatformHeadlessInAppWebViewCreationParams( initialSettings: InAppWebViewSettings( userAgent: Utils.getRandomUA(), mediaPlaybackRequiresUserGesture: true, cacheEnabled: true, blockNetworkImage: false, loadsImagesAutomatically: true, upgradeKnownHostsToHTTPS: false, safeBrowsingEnabled: false, ), onWebViewCreated: (controller) { logEventController.add('[Captcha WebView] Created'); webviewController = controller; initEventController.add(true); }, onLoadStart: (controller, url) { logEventController.add('[Captcha WebView] Load start: $url'); }, onLoadStop: (controller, url) { logEventController.add('[Captcha WebView] Load stop: $url'); if (buttonWasClicked && !captchaDisappearedController.isClosed) { KazumiLogger().i('[Captcha WebView] Button click → page navigated, verification done'); buttonWasClicked = false; captchaDisappearedController.add(null); } }, onReceivedError: (controller, request, error) { logEventController .add('[Captcha WebView] Error: ${error.description}'); }, ), ); await _headlessWebView!.run(); } void _registerHandlers() { if (_handlersRegistered) return; webviewController?.addJavaScriptHandler( handlerName: 'CaptchaImageBridge', callback: (args) { final src = args.isNotEmpty ? args[0].toString() : ''; logEventController.add('[Captcha WebView] Captcha image found: $src'); if (src.isNotEmpty && !captchaImageFoundController.isClosed) { captchaWasFound = true; captchaImageFoundController.add(src); } }, ); webviewController?.addJavaScriptHandler( handlerName: 'CaptchaStatusBridge', callback: (args) { final status = args.isNotEmpty ? args[0].toString() : ''; logEventController.add('[Captcha WebView JS] Page captcha status: $status'); if (status == 'absent' && captchaWasFound && !captchaDisappearedController.isClosed) { KazumiLogger().i('[Captcha WebView] Captcha gone after navigation (StatusBridge)'); captchaWasFound = false; captchaDisappearedController.add(null); } }, ); webviewController?.addJavaScriptHandler( handlerName: 'CaptchaGoneBridge', callback: (args) { logEventController.add('[Captcha WebView] Captcha image disappeared'); buttonWasClicked = false; if (!captchaDisappearedController.isClosed) { captchaDisappearedController.add(null); } }, ); webviewController?.addJavaScriptHandler( handlerName: 'ButtonClickedBridge', callback: (args) { logEventController.add('[Captcha WebView] Button clicked flag set'); buttonWasClicked = true; }, ); webviewController?.addJavaScriptHandler( handlerName: 'CaptchaLogBridge', callback: (args) { if (args.isNotEmpty) { logEventController.add('[Captcha WebView JS] ${args[0]}'); } }, ); _handlersRegistered = true; logEventController.add('[Captcha WebView] JS handlers registered'); } Future _addCaptchaUserScript() async { if (_currentCaptchaImageXpath.isEmpty) return; final escapedXpath = _currentCaptchaImageXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedInputXpath = _currentInputXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); // Remove any previously injected captcha script before adding a fresh one. await webviewController?.removeAllUserScripts(); const String scriptTemplate = """ window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'CaptchaScript loaded on: ' + window.location.href); var _captchaXpath = '{XPATH}'; var _inputXpath = '{INPUT_XPATH}'; var _captchaPoller = null; var _disappearObserver = null; function _evalXpath() { try { var result = document.evaluate( _captchaXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } catch(e) { return null; } } function _startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { var node = _evalXpath(); if (!node) { _disappearObserver.disconnect(); _disappearObserver = null; try { window.flutter_inappwebview.callHandler('CaptchaGoneBridge', ''); } catch(e) {} } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function _captureAsBase64(imgNode, callback) { function doCapture() { try { var canvas = document.createElement('canvas'); canvas.width = imgNode.naturalWidth || imgNode.width || 100; canvas.height = imgNode.naturalHeight || imgNode.height || 40; var ctx = canvas.getContext('2d'); ctx.drawImage(imgNode, 0, 0); callback(canvas.toDataURL('image/png')); } catch(e) { callback(null); } } if (imgNode.complete && imgNode.naturalWidth > 0) { doCapture(); } else { imgNode.addEventListener('load', doCapture); imgNode.addEventListener('error', function() { callback(null); }); } } function _checkForCaptcha() { var node = _evalXpath(); if (node) { _captureAsBase64(node, function(dataUrl) { if (dataUrl) { try { window.flutter_inappwebview.callHandler('CaptchaImageBridge', dataUrl); } catch(e) {} } }); _startDisappearMonitor(); return true; } return false; } function _triggerInputFocus() { if (!_inputXpath) return false; try { var inputResult = document.evaluate(_inputXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var inputEl = inputResult.singleNodeValue; if (inputEl) { if (typeof \$ !== 'undefined' && \$) { \$(inputEl).trigger('focus'); return true; } else if (typeof jQuery !== 'undefined' && jQuery) { jQuery(inputEl).trigger('focus'); return true; } else { inputEl.focus(); return true; } } } catch(e) { try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Failed to trigger input focus - ' + e.message); } catch(e2) {} } return false; } // Report captcha status to Dart at DOMContentLoaded so that after a full-page // navigation Dart can detect that verification succeeded (captcha is gone). // Also trigger input focus here since DOM is ready at this point. window.addEventListener('DOMContentLoaded', function() { _triggerInputFocus(); var node = _evalXpath(); try { window.flutter_inappwebview.callHandler('CaptchaStatusBridge', node ? 'present' : 'absent'); } catch(e) {} }); if (!_checkForCaptcha()) { _captchaPoller = setInterval(function() { if (_checkForCaptcha()) { clearInterval(_captchaPoller); _captchaPoller = null; } }, 500); } """; final script = scriptTemplate .replaceAll('{XPATH}', escapedXpath) .replaceAll('{INPUT_XPATH}', escapedInputXpath); await webviewController?.addUserScripts( userScripts: [ UserScript( source: script, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, ), ], ); } @override Future loadPage(String url, String captchaXpath, {String? inputXpath}) async { _currentCaptchaImageXpath = captchaXpath; _currentInputXpath = inputXpath ?? ''; captchaWasFound = false; buttonWasClicked = false; _registerHandlers(); await _addCaptchaUserScript(); try { await PlatformCookieManager(const PlatformCookieManagerCreationParams()) .deleteAllCookies(); logEventController.add('[Captcha WebView] Cookies cleared before load'); } catch (_) {} await webviewController ?.loadUrl(urlRequest: URLRequest(url: WebUri(url))); } @override Future loadPageForButtonClick(String url, String buttonXpath) async { _currentCaptchaImageXpath = ''; // disable captcha-image script on navigation captchaWasFound = false; buttonWasClicked = false; _registerHandlers(); await _addButtonClickUserScript(buttonXpath); try { await PlatformCookieManager(const PlatformCookieManagerCreationParams()) .deleteAllCookies(); logEventController.add('[Captcha WebView] Cookies cleared before load'); } catch (_) {} await webviewController ?.loadUrl(urlRequest: URLRequest(url: WebUri(url))); } Future _addButtonClickUserScript(String buttonXpath) async { final escapedXpath = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); await webviewController?.removeAllUserScripts(); const String scriptTemplate = """ try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'ButtonClickScript loaded on: ' + window.location.href); } catch(e) {} var _btnXpath = '{XPATH}'; var _clicked = false; var _poller = null; var _disappearObserver = null; function _evalBtnXpath() { try { var result = document.evaluate( _btnXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } catch(e) { return null; } } function _startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { if (!_evalBtnXpath()) { _disappearObserver.disconnect(); _disappearObserver = null; try { window.flutter_inappwebview.callHandler('CaptchaGoneBridge', ''); } catch(e) {} } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function _checkAndClick() { var btn = _evalBtnXpath(); if (btn && !_clicked) { _clicked = true; btn.click(); try { window.flutter_inappwebview.callHandler('ButtonClickedBridge', ''); } catch(e) {} _startDisappearMonitor(); return true; } return false; } if (!_checkAndClick()) { _poller = setInterval(function() { if (_checkAndClick()) { clearInterval(_poller); _poller = null; } }, 500); } """; final script = scriptTemplate.replaceAll('{XPATH}', escapedXpath); await webviewController?.addUserScripts( userScripts: [ UserScript( source: script, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END, ), ], ); } @override Future submitCaptchaInteract( String captchaCode, String inputXpath, String buttonXpath) async { logEventController .add('[Captcha WebView] Filling input and clicking button'); final escapedCode = captchaCode.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedInput = inputXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedButton = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { function evalXpath(xpath) { try { var r = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch(e) { return null; } } var inputEl = evalXpath('$escapedInput'); if (inputEl) { inputEl.focus(); var nativeInput = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value'); nativeInput.set.call(inputEl, '$escapedCode'); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Input filled'); } catch(e) {} } else { try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Input element not found'); } catch(e) {} } var btnEl = evalXpath('$escapedButton'); if (btnEl) { btnEl.click(); try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Button clicked'); } catch(e) {} } else { try { window.flutter_inappwebview.callHandler('CaptchaLogBridge', 'Button element not found'); } catch(e) {} } })(); '''; await webviewController?.evaluateJavascript(source: script); } @override Future getCookieString(String pageUrl) async { try { final PlatformCookieManager cookieManager = PlatformCookieManager( PlatformCookieManagerCreationParams(), ); final cookies = await cookieManager.getCookies(url: WebUri(pageUrl)); return cookies.map((c) => '${c.name}=${c.value}').join('; '); } catch (e) { KazumiLogger().e('[Captcha WebView] getCookieString error: $e'); return ''; } } @override Future unloadPage() async { try { await webviewController ?.loadUrl(urlRequest: URLRequest(url: WebUri('about:blank'))); } catch (_) {} } @override void dispose() { _currentCaptchaImageXpath = ''; _currentInputXpath = ''; captchaWasFound = false; buttonWasClicked = false; _handlersRegistered = false; try { PlatformCookieManager(const PlatformCookieManagerCreationParams()) .deleteAllCookies(); } catch (_) {} try { captchaImageFoundController.close(); captchaDisappearedController.close(); initEventController.close(); logEventController.close(); } catch (_) {} _headlessWebView?.dispose(); _headlessWebView = null; webviewController = null; } } ================================================ FILE: lib/webview/captcha/impl/captcha_webview_linux_impl.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/webview/captcha/captcha_webview_controller.dart'; class CaptchaWebviewLinuxImpl extends CaptchaWebviewController { VoidCallback? _navigationListener; String _currentCaptchaImageXpath = ''; String _buttonXpath = ''; @override Future init() async { final proxyConfig = _getProxyConfiguration(); webviewController ??= await WebviewWindow.create( configuration: CreateConfiguration( headless: true, proxy: proxyConfig, ), ); _initMessageBridge(); _initNavigationListener(); initEventController.add(true); } ProxyConfiguration? _getProxyConfiguration() { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) return null; final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final parsed = ProxyUtils.parseProxyUrl(proxyUrl); if (parsed == null) return null; final (host, port) = parsed; KazumiLogger().i('[Captcha WebView] 代理设置成功 $host:$port'); return ProxyConfiguration(host: host, port: port); } void _initMessageBridge() { webviewController?.addOnWebMessageReceivedCallback((message) async { final msg = message.toString(); logEventController.add('[Captcha WebView] WM: $msg'); if (msg.startsWith('captchaImage:')) { final src = msg.replaceFirst('captchaImage:', ''); if (src.isNotEmpty && !captchaImageFoundController.isClosed) { captchaWasFound = true; captchaImageFoundController.add(src); } } else if (msg.startsWith('buttonClicked:')) { buttonWasClicked = true; logEventController.add('[Captcha WebView] Button clicked flag set'); } else if (msg.startsWith('captchaGone:')) { buttonWasClicked = false; if (!captchaDisappearedController.isClosed) { captchaDisappearedController.add(null); } } else if (msg.startsWith('captchaLog:')) { logEventController.add( '[Captcha WebView JS] ${msg.replaceFirst('captchaLog:', '')}'); } }); } void _initNavigationListener() { _navigationListener = () { _onNavigationInject(); _onNavigationCompletion(); }; webviewController?.isNavigating.addListener(_navigationListener!); } Future _onNavigationInject() async { if (webviewController?.isNavigating.value == false) { logEventController.add('[Captcha WebView] Navigation completed'); if (_currentCaptchaImageXpath.isNotEmpty) { await _injectCaptchaScript(); } else if (_buttonXpath.isNotEmpty) { await _injectButtonClickScript(_buttonXpath); } } } Future _onNavigationCompletion() async { if (webviewController?.isNavigating.value == false) { // Type-1: captcha image was seen; check if it has disappeared. if (captchaWasFound) { final present = await _isCaptchaPresent(); if (!present && !captchaDisappearedController.isClosed) { logEventController .add('[Captcha WebView] Captcha gone after navigation'); captchaWasFound = false; captchaDisappearedController.add(null); } } // Type-2: button was clicked; page navigation confirms verification. if (buttonWasClicked && !captchaDisappearedController.isClosed) { logEventController.add( '[Captcha WebView] Button click and page navigated, verification done'); buttonWasClicked = false; captchaDisappearedController.add(null); } } } Future _isCaptchaPresent() async { if (_currentCaptchaImageXpath.isEmpty || webviewController == null) return false; final escaped = _currentCaptchaImageXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); try { final result = await webviewController!.evaluateJavaScript(''' (function() { try { var r = document.evaluate('$escaped', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue ? 'present' : 'absent'; } catch(e) { return 'absent'; } })(); '''); return result?.contains('present') ?? false; } catch (e) { KazumiLogger().d('[Captcha WebView] _isCaptchaPresent error: $e'); return false; } } Future _injectCaptchaScript() async { if (_currentCaptchaImageXpath.isEmpty) return; final escapedXpath = _currentCaptchaImageXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { window.webkit.messageHandlers.msgToNative.postMessage( 'captchaLog:CaptchaScript injected on ' + window.location.href); var _captchaXpath = '$escapedXpath'; var _captchaPoller = null; var _disappearObserver = null; function _resolveSrc(node) { return node.getAttribute('src') || node.getAttribute('data-src') || node.src || ''; } function _evalXpath() { try { var result = document.evaluate( _captchaXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } catch(e) { return null; } } function _startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { if (!_evalXpath()) { _disappearObserver.disconnect(); _disappearObserver = null; window.webkit.messageHandlers.msgToNative.postMessage('captchaGone:'); } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function _captureAsBase64(imgNode, callback) { function doCapture() { try { var canvas = document.createElement('canvas'); canvas.width = imgNode.naturalWidth || imgNode.width || 100; canvas.height = imgNode.naturalHeight || imgNode.height || 40; var ctx = canvas.getContext('2d'); ctx.drawImage(imgNode, 0, 0); callback(canvas.toDataURL('image/png')); } catch(e) { callback(null); } } if (imgNode.complete && imgNode.naturalWidth > 0) { doCapture(); } else { imgNode.addEventListener('load', doCapture); imgNode.addEventListener('error', function() { callback(null); }); } } function _checkForCaptcha() { var node = _evalXpath(); if (node) { _captureAsBase64(node, function(dataUrl) { if (dataUrl) { window.webkit.messageHandlers.msgToNative.postMessage('captchaImage:' + dataUrl); } }); _startDisappearMonitor(); return true; } return false; } if (!_checkForCaptcha()) { _captchaPoller = setInterval(function() { if (_checkForCaptcha()) { clearInterval(_captchaPoller); _captchaPoller = null; } }, 500); } })(); '''; try { await webviewController?.evaluateJavaScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] inject script error: $e'); } } @override Future loadPage(String url, String captchaXpath, {String? inputXpath}) async { _currentCaptchaImageXpath = captchaXpath; _buttonXpath = ''; buttonWasClicked = false; captchaWasFound = false; webviewController?.launch(url); } @override Future loadPageForButtonClick(String url, String buttonXpath) async { _currentCaptchaImageXpath = ''; _buttonXpath = buttonXpath; buttonWasClicked = false; captchaWasFound = false; webviewController?.launch(url); } Future _injectButtonClickScript(String buttonXpath) async { final escaped = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { window.webkit.messageHandlers.msgToNative.postMessage( 'captchaLog:ButtonClickScript injected on ' + window.location.href); var _xpath = '$escaped'; var _clicked = false; var _poller = null; var _disappearObserver = null; function evalXpath() { try { var r = document.evaluate(_xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch(e) { return null; } } function startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { if (!evalXpath()) { _disappearObserver.disconnect(); _disappearObserver = null; window.webkit.messageHandlers.msgToNative.postMessage('captchaGone:'); } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function checkAndClick() { var btn = evalXpath(); if (btn && !_clicked) { _clicked = true; btn.click(); window.webkit.messageHandlers.msgToNative.postMessage('buttonClicked:'); startDisappearMonitor(); return true; } return false; } if (!checkAndClick()) { _poller = setInterval(function() { if (checkAndClick()) { clearInterval(_poller); _poller = null; } }, 500); } })(); '''; try { await webviewController?.evaluateJavaScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] injectButtonClickScript error: $e'); } } @override Future submitCaptchaInteract( String captchaCode, String inputXpath, String buttonXpath) async { logEventController .add('[Captcha WebView] Filling input and clicking button'); final escapedCode = captchaCode.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedInput = inputXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedButton = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { function evalXpath(xpath) { try { var r = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch(e) { return null; } } var inputEl = evalXpath('$escapedInput'); if (inputEl) { inputEl.focus(); var nativeInput = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value'); nativeInput.set.call(inputEl, '$escapedCode'); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Input filled'); } else { window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Input element not found'); } var btnEl = evalXpath('$escapedButton'); if (btnEl) { btnEl.click(); window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Button clicked'); } else { window.webkit.messageHandlers.msgToNative.postMessage('captchaLog:Button element not found'); } })(); '''; try { await webviewController?.evaluateJavaScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] submitCaptchaInteract error: $e'); } } @override Future getCookieString(String pageUrl) async { try { final cookies = await webviewController?.getAllCookies() ?? []; final cookieString = cookies.map((c) => '${c.name}=${c.value}').join('; '); logEventController .add('[Captcha WebView] Cookies: $cookieString'); return cookieString; } catch (e) { KazumiLogger().e('[Captcha WebView] getCookieString error: $e'); return ''; } } @override Future unloadPage() async { webviewController?.launch('about:blank'); } @override void dispose() { _currentCaptchaImageXpath = ''; _buttonXpath = ''; buttonWasClicked = false; captchaWasFound = false; if (_navigationListener != null) { try { webviewController?.isNavigating.removeListener(_navigationListener!); } catch (_) {} _navigationListener = null; } try { captchaImageFoundController.close(); captchaDisappearedController.close(); initEventController.close(); logEventController.close(); } catch (_) {} webviewController?.close(); webviewController = null; } } ================================================ FILE: lib/webview/captcha/impl/captcha_webview_windows_impl.dart ================================================ import 'dart:async'; import 'package:webview_windows/webview_windows.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/webview/captcha/captcha_webview_controller.dart'; class CaptchaWebviewWindowsImpl extends CaptchaWebviewController { HeadlessWebview? _headlessWebview; final List _subscriptions = []; String _currentCaptchaImageXpath = ''; String _currentInputXpath = ''; String _currentPageUrl = ''; String _buttonXpath = ''; @override Future init() async { await _setupProxy(); _headlessWebview ??= HeadlessWebview(); await _headlessWebview!.run(); await _headlessWebview!.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny); // Listen for messages from JavaScript via window.chrome.webview.postMessage _subscriptions.add( _headlessWebview!.webMessage.listen(_onWebMessage), ); // Inject captcha or button-click script when navigation completes _subscriptions.add( _headlessWebview!.loadingState.listen((state) async { if (state == LoadingState.navigationCompleted) { logEventController .add('[Captcha WebView] Navigation completed: $_currentPageUrl'); if (_currentCaptchaImageXpath.isNotEmpty) { await _injectCaptchaScript(); } else if (_buttonXpath.isNotEmpty) { await _injectButtonClickScript(_buttonXpath); } } }), ); // After a navigation, detect verification completion for both type-1 // (captcha image gone) and type-2 (button was clicked, page navigated). _subscriptions.add( _headlessWebview!.loadingState.listen((state) async { if (state == LoadingState.navigationCompleted) { if (captchaWasFound) { final present = await _isCaptchaPresent(); if (!present && !captchaDisappearedController.isClosed) { logEventController .add('[Captcha WebView] Captcha gone after navigation'); captchaWasFound = false; captchaDisappearedController.add(null); } } if (buttonWasClicked && !captchaDisappearedController.isClosed) { logEventController .add('[Captcha WebView] Button click → page navigated, verification done'); buttonWasClicked = false; captchaDisappearedController.add(null); } } }), ); initEventController.add(true); } void _onWebMessage(dynamic message) { final msg = message.toString(); logEventController.add('[Captcha WebView] WM: $msg'); if (msg.startsWith('captchaImage:')) { final src = msg.replaceFirst('captchaImage:', ''); if (src.isNotEmpty && !captchaImageFoundController.isClosed) { captchaWasFound = true; captchaImageFoundController.add(src); } } else if (msg.startsWith('buttonClicked:')) { buttonWasClicked = true; logEventController.add('[Captcha WebView] Button clicked flag set'); } else if (msg.startsWith('captchaGone:')) { buttonWasClicked = false; if (!captchaDisappearedController.isClosed) { captchaDisappearedController.add(null); } } else if (msg.startsWith('captchaLog:')) { logEventController.add('[Captcha WebView JS] ${msg.replaceFirst('captchaLog:', '')}'); } } Future _isCaptchaPresent() async { if (_currentCaptchaImageXpath.isEmpty || _headlessWebview == null) return false; final escaped = _currentCaptchaImageXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); try { final result = await _headlessWebview!.executeScript(''' (function() { try { var r = document.evaluate('$escaped', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue ? 'present' : 'absent'; } catch(e) { return 'absent'; } })(); '''); return result?.toString().contains('present') ?? false; } catch (e) { KazumiLogger().d('[Captcha WebView] _isCaptchaPresent error: $e'); return false; } } Future _injectCaptchaScript() async { if (_currentCaptchaImageXpath.isEmpty) return; final escapedXpath = _currentCaptchaImageXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedInputXpath = _currentInputXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { window.chrome.webview.postMessage('captchaLog:CaptchaScript injected on ' + window.location.href); var _captchaXpath = '$escapedXpath'; var _inputXpath = '$escapedInputXpath'; var _captchaPoller = null; var _disappearObserver = null; function _resolveSrc(node) { return node.getAttribute('src') || node.getAttribute('data-src') || node.src || ''; } function _evalXpath() { try { var result = document.evaluate( _captchaXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } catch(e) { return null; } } function _startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { if (!_evalXpath()) { _disappearObserver.disconnect(); _disappearObserver = null; window.chrome.webview.postMessage('captchaGone:'); } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function _captureAsBase64(imgNode, callback) { function doCapture() { try { var canvas = document.createElement('canvas'); canvas.width = imgNode.naturalWidth || imgNode.width || 100; canvas.height = imgNode.naturalHeight || imgNode.height || 40; var ctx = canvas.getContext('2d'); ctx.drawImage(imgNode, 0, 0); callback(canvas.toDataURL('image/png')); } catch(e) { callback(null); } } if (imgNode.complete && imgNode.naturalWidth > 0) { doCapture(); } else { imgNode.addEventListener('load', doCapture); imgNode.addEventListener('error', function() { callback(null); }); } } function _checkForCaptcha() { var node = _evalXpath(); if (node) { _captureAsBase64(node, function(dataUrl) { if (dataUrl) { window.chrome.webview.postMessage('captchaImage:' + dataUrl); } }); _startDisappearMonitor(); return true; } return false; } function _triggerInputFocus() { if (!_inputXpath) { return false; } try { var inputResult = document.evaluate(_inputXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var inputEl = inputResult.singleNodeValue; if (inputEl) { if (typeof \$ !== 'undefined' && \$) { \$(inputEl).trigger('focus'); return true; } else if (typeof jQuery !== 'undefined' && jQuery) { jQuery(inputEl).trigger('focus'); return true; } else { inputEl.focus(); return true; } } } catch(e) { window.chrome.webview.postMessage('captchaLog:Failed to trigger input focus - ' + e.message); } return false; } // If inputXpath is provided, trigger focus to load captcha (some sites require this) _triggerInputFocus(); if (!_checkForCaptcha()) { _captchaPoller = setInterval(function() { if (_checkForCaptcha()) { clearInterval(_captchaPoller); _captchaPoller = null; } }, 500); } })(); '''; try { await _headlessWebview?.executeScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] inject script error: $e'); } } @override Future loadPage(String url, String captchaXpath, {String? inputXpath}) async { _currentCaptchaImageXpath = captchaXpath; _currentInputXpath = inputXpath ?? ''; _buttonXpath = ''; buttonWasClicked = false; _currentPageUrl = url; captchaWasFound = false; await _headlessWebview?.loadUrl(url); } @override Future loadPageForButtonClick(String url, String buttonXpath) async { _currentCaptchaImageXpath = ''; _buttonXpath = buttonXpath; buttonWasClicked = false; _currentPageUrl = url; captchaWasFound = false; await _headlessWebview?.loadUrl(url); } Future _injectButtonClickScript(String buttonXpath) async { final escaped = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { window.chrome.webview.postMessage('captchaLog:ButtonClickScript injected on ' + window.location.href); var _xpath = '$escaped'; var _clicked = false; var _poller = null; var _disappearObserver = null; function evalXpath() { try { var r = document.evaluate(_xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch(e) { return null; } } function startDisappearMonitor() { if (_disappearObserver) return; _disappearObserver = new MutationObserver(function() { if (!evalXpath()) { _disappearObserver.disconnect(); _disappearObserver = null; window.chrome.webview.postMessage('captchaGone:'); } }); _disappearObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); } function checkAndClick() { var btn = evalXpath(); if (btn && !_clicked) { _clicked = true; btn.click(); window.chrome.webview.postMessage('buttonClicked:'); startDisappearMonitor(); return true; } return false; } if (!checkAndClick()) { _poller = setInterval(function() { if (checkAndClick()) { clearInterval(_poller); _poller = null; } }, 500); } })(); '''; try { await _headlessWebview?.executeScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] injectButtonClickScript error: $e'); } } @override Future submitCaptchaInteract( String captchaCode, String inputXpath, String buttonXpath) async { logEventController .add('[Captcha WebView] Filling input and clicking button'); final escapedCode = captchaCode.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedInput = inputXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final escapedButton = buttonXpath.replaceAll('\\', '\\\\').replaceAll("'", "\\'"); final script = ''' (function() { function evalXpath(xpath) { try { var r = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch(e) { return null; } } var inputEl = evalXpath('$escapedInput'); if (inputEl) { inputEl.focus(); var nativeInput = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value'); nativeInput.set.call(inputEl, '$escapedCode'); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); window.chrome.webview.postMessage('captchaLog:Input filled'); } else { window.chrome.webview.postMessage('captchaLog:Input element not found'); } var btnEl = evalXpath('$escapedButton'); if (btnEl) { btnEl.click(); window.chrome.webview.postMessage('captchaLog:Button clicked'); } else { window.chrome.webview.postMessage('captchaLog:Button element not found'); } })(); '''; try { await _headlessWebview?.executeScript(script); } catch (e) { KazumiLogger().e('[Captcha WebView] submitCaptchaInteract error: $e'); } } @override Future getCookieString(String pageUrl) async { try { final result = await _headlessWebview?.getCookies(pageUrl); return result ?? ''; } catch (e) { KazumiLogger().e('[Captcha WebView] getCookieString error: $e'); return ''; } } @override Future unloadPage() async { try { await _headlessWebview?.executeScript( "window.location.href = 'about:blank';"); } catch (e) { KazumiLogger().d('[Captcha WebView] unloadPage skipped: $e'); } } @override void dispose() { _currentCaptchaImageXpath = ''; _currentInputXpath = ''; _buttonXpath = ''; buttonWasClicked = false; _currentPageUrl = ''; for (final s in _subscriptions) { try { s.cancel(); } catch (_) {} } _subscriptions.clear(); try { captchaImageFoundController.close(); captchaDisappearedController.close(); initEventController.close(); logEventController.close(); } catch (_) {} _headlessWebview?.dispose(); _headlessWebview = null; } Future _setupProxy() async { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) return; final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl); if (formattedProxy == null) return; try { await WebviewController.initializeEnvironment( additionalArguments: '--proxy-server=$formattedProxy', ); KazumiLogger().i('[Captcha WebView] 代理设置成功 $formattedProxy'); } catch (e) { KazumiLogger().e('[Captcha WebView] 设置代理失败 $e'); } } } ================================================ FILE: lib/webview/video/impl/video_webview_android_impl.dart ================================================ import 'dart:async'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; import 'package:flutter_inappwebview_android/flutter_inappwebview_android.dart' as android_webview; class VideoWebviewAndroidImpl extends VideoWebviewController { PlatformHeadlessInAppWebView? headlessWebView; bool hasInjectedScripts = false; bool shouldInjectIframeRedirect = false; @override Future init() async { await _setupProxy(); headlessWebView ??= PlatformHeadlessInAppWebView( PlatformHeadlessInAppWebViewCreationParams( initialSettings: InAppWebViewSettings( userAgent: Utils.getRandomUA(), mediaPlaybackRequiresUserGesture: true, cacheEnabled: false, blockNetworkImage: true, loadsImagesAutomatically: false, upgradeKnownHostsToHTTPS: false, safeBrowsingEnabled: false, mixedContentMode: MixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE, geolocationEnabled: false, ), onWebViewCreated: (controller) { print('[WebView] Created'); webviewController = controller; initEventController.add(true); }, onLoadStart: (controller, url) async { logEventController.add('started loading: $url'); }, onLoadStop: (controller, url) { logEventController.add('loading completed: $url'); }, ), ); await headlessWebView?.run(); } @override Future loadUrl(String url, bool useLegacyParser, {int offset = 0}) async { await unloadPage(); if (!hasInjectedScripts) { addJavaScriptHandlers(useLegacyParser); await addUserScripts(useLegacyParser); hasInjectedScripts = true; } count = 0; this.offset = offset; isIframeLoaded = false; isVideoSourceLoaded = false; shouldInjectIframeRedirect = true; videoLoadingEventController.add(true); await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url))); } void addJavaScriptHandlers(bool useLegacyParser) { logEventController.add('Adding LogBridge handler'); webviewController?.addJavaScriptHandler( handlerName: 'LogBridge', callback: (args) { String message = args[0].toString(); if (message.contains('about:blank')) { return; } logEventController.add(message); }); if (useLegacyParser) { logEventController.add('Adding JSBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'JSBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); logEventController.add( 'If there is audio but no video, please report it to the rule developer.'); if ((message.contains('http') || message.startsWith('//')) && !message.contains('googleads') && !message.contains('googlesyndication.com') && !message.contains('prestrain.html') && !message.contains('prestrain%2Ehtml') && !message.contains('adtrafficquality')) { logEventController.add('Parsing video source $message'); String encodedUrl = Uri.encodeFull(message); if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) { isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add( 'Loading video source ${Utils.decodeVideoSource(encodedUrl)}'); unloadPage(); videoParserEventController .add((Utils.decodeVideoSource(encodedUrl), offset)); } } }); } else { logEventController.add('Adding VideoBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'VideoBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); if (message.contains('http') && !isVideoSourceLoaded) { logEventController.add('Loading video source: $message'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); unloadPage(); videoParserEventController.add((message, offset)); } }); } } Future addUserScripts(bool useLegacyParser) async { final List scripts = []; if (useLegacyParser) { logEventController.add('Adding JSBridgeDebug UserScript'); const String jsBridgeDebugScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href); function processIframeElement(iframe) { window.flutter_inappwebview.callHandler('LogBridge', 'Processing iframe element'); let src = iframe.getAttribute('src'); if (src) { window.flutter_inappwebview.callHandler('JSBridgeDebug', src); } } const _observer = new MutationObserver((mutations) => { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for iframes...'); mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.target.nodeName === 'IFRAME') { processIframeElement(mutation.target); } else { mutation.addedNodes.forEach(node => { if (node.nodeName === 'IFRAME') processIframeElement(node); if (node.querySelectorAll) { node.querySelectorAll('iframe').forEach(processIframeElement); } }); } }); }); _observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); """; scripts.add(UserScript( source: jsBridgeDebugScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, )); } else { logEventController.add('Adding VideoBridgeDebug UserScripts'); const String blobParserScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href); const _r_text = window.Response.prototype.text; window.Response.prototype.text = function () { return new Promise((resolve, reject) => { _r_text.call(this).then((text) => { resolve(text); if (text.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url); window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url); } }).catch(reject); }); } const _open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (content.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]); window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]); }; } catch {} }); return _open.apply(this, args); }; """; const String videoTagParserScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href); const _observer = new MutationObserver((mutations) => { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...'); for (const mutation of mutations) { if (mutation.type === "attributes" && mutation.target.nodeName === "VIDEO") { if (processVideoElement(mutation.target)) return; continue; } for (const node of mutation.addedNodes) { if (node.nodeName === "VIDEO") { if (processVideoElement(node)) return; } if (node.querySelectorAll) { for (const video of node.querySelectorAll("video")) { if (processVideoElement(video)) return; } } } } }); function processVideoElement(video) { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL'); let src = video.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { _observer.disconnect(); window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return true; } const sources = video.getElementsByTagName('source'); for (let source of sources) { src = source.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { _observer.disconnect(); window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return true; } } } function setupVideoProcessing() { for (const video of document.querySelectorAll("video")) { if (processVideoElement(video)) return; } _observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupVideoProcessing); } else { setupVideoProcessing(); } """; scripts.add(UserScript( source: blobParserScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, )); scripts.add(UserScript( source: videoTagParserScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, )); } await webviewController?.addUserScripts( userScripts: scripts, ); } @override Future unloadPage() async { await webviewController! .loadUrl(urlRequest: URLRequest(url: WebUri("about:blank"))); } @override void dispose() { headlessWebView?.dispose(); headlessWebView = null; webviewController = null; } Future _setupProxy() async { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) { return; } final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl); if (formattedProxy == null) { return; } try { final proxyAvailable = await android_webview.AndroidWebViewFeature.instance() .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE); if (!proxyAvailable) { KazumiLogger().w('WebView: 当前 Android 版本不支持代理'); return; } final proxyController = android_webview.AndroidProxyController.instance(); await proxyController.clearProxyOverride(); await proxyController.setProxyOverride( settings: ProxySettings( proxyRules: [ ProxyRule(url: formattedProxy), ], ), ); KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); } catch (e) { KazumiLogger().e('WebView: 设置代理失败 $e'); } } } ================================================ FILE: lib/webview/video/impl/video_webview_apple_impl.dart ================================================ import 'dart:async'; import 'dart:collection'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; class VideoWebviewAppleImpl extends VideoWebviewController { PlatformHeadlessInAppWebView? headlessWebView; bool hasInjectedScripts = false; @override Future init() async { headlessWebView ??= PlatformHeadlessInAppWebView( PlatformHeadlessInAppWebViewCreationParams( initialUserScripts: UnmodifiableListView([ UserScript( source: ''' function removeLazyLoading() { document.querySelectorAll('iframe[loading="lazy"]').forEach(iframe => { console.log('Removing lazy loading from:', iframe.src); iframe.removeAttribute('loading'); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', removeLazyLoading); } else { removeLazyLoading(); } ''', injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END, ), ]), initialSettings: InAppWebViewSettings( userAgent: Utils.getRandomUA(), mediaPlaybackRequiresUserGesture: true, useOnLoadResource: false, cacheEnabled: false, isInspectable: false, contentBlockers: [ ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?devtools-detector\.js", resourceType: [ ContentBlockerTriggerResourceType.SCRIPT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger(urlFilter: '.*', resourceType: [ ContentBlockerTriggerResourceType.IMAGE, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?googleads", resourceType: [ ContentBlockerTriggerResourceType.DOCUMENT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?googlesyndication\.com", resourceType: [ ContentBlockerTriggerResourceType.DOCUMENT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?prestrain\.html", resourceType: [ ContentBlockerTriggerResourceType.DOCUMENT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?prestrain%2Ehtml", resourceType: [ ContentBlockerTriggerResourceType.DOCUMENT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ContentBlocker( trigger: ContentBlockerTrigger( urlFilter: r"^https?://.+?adtrafficquality", resourceType: [ ContentBlockerTriggerResourceType.DOCUMENT, ]), action: ContentBlockerAction(type: ContentBlockerActionType.BLOCK), ), ], ), onWebViewCreated: (controller) { KazumiLogger().i('WebView: created'); webviewController = controller; initEventController.add(true); }, onLoadStart: (controller, url) { logEventController.add('started loading: $url'); }, onLoadStop: (controller, url) { logEventController.add('loading completed: $url'); }, onReceivedError: (controller, request, error) { KazumiLogger().e('WebView: error: ${error.toString()} - Request: ${request.url}'); }, ), ); await headlessWebView?.run(); } @override Future loadUrl(String url, bool useLegacyParser, {int offset = 0}) async { await unloadPage(); if (!hasInjectedScripts) { addJavaScriptHandlers(useLegacyParser); await addUserScripts(useLegacyParser); hasInjectedScripts = true; } count = 0; this.offset = offset; isIframeLoaded = false; isVideoSourceLoaded = false; videoLoadingEventController.add(true); await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url))); } void addJavaScriptHandlers(bool useLegacyParser) { logEventController.add('Adding LogBridge handler'); webviewController?.addJavaScriptHandler( handlerName: 'LogBridge', callback: (args) { String message = args[0].toString(); if (message.contains('about:blank')) { return; } logEventController.add(message); }); if (useLegacyParser) { logEventController.add('Adding JSBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'JSBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); logEventController.add( 'If there is audio but no video, please report it to the rule developer.'); if ((message.contains('http') || message.startsWith('//')) && !message.contains('googleads') && !message.contains('googlesyndication.com') && !message.contains('prestrain.html') && !message.contains('prestrain%2Ehtml') && !message.contains('adtrafficquality')) { logEventController.add('Parsing video source $message'); String encodedUrl = Uri.encodeFull(message); if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) { isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add( 'Loading video source ${Utils.decodeVideoSource(encodedUrl)}'); unloadPage(); videoParserEventController .add((Utils.decodeVideoSource(encodedUrl), offset)); } } }); } else { logEventController.add('Adding VideoBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'VideoBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); if (message.contains('http') && !isVideoSourceLoaded) { logEventController.add('Loading video source: $message'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); unloadPage(); videoParserEventController.add((message, offset)); } }); } } Future addUserScripts( bool useLegacyParser) async { final List scripts = []; if (useLegacyParser) { logEventController.add('Adding JSBridgeDebug UserScript'); const String jsBridgeDebugScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href); var iframes = document.getElementsByTagName('iframe'); window.flutter_inappwebview.callHandler('LogBridge', 'The number of iframe tags is ' + iframes.length); for (var i = 0; i < iframes.length; i++) { var iframe = iframes[i]; var src = iframe.getAttribute('src'); if (src) { window.flutter_inappwebview.callHandler('JSBridgeDebug', src); } } """; scripts.add(UserScript( source: jsBridgeDebugScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END, forMainFrameOnly: false, )); } else { logEventController.add('Adding VideoBridgeDebug UserScripts'); const String blobParserScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href); const _r_text = window.Response.prototype.text; window.Response.prototype.text = function () { return new Promise((resolve, reject) => { _r_text.call(this).then((text) => { resolve(text); if (text.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url); window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url); } }).catch(reject); }); } const _open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (content.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]); window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]); }; } catch {} }); return _open.apply(this, args); }; """; const String videoTagParserScript = """ window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href); function processVideoElement(video) { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL'); let src = video.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return; } const sources = video.getElementsByTagName('source'); for (let source of sources) { src = source.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return; } } } document.querySelectorAll('video').forEach(processVideoElement); const _observer = new MutationObserver((mutations) => { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...'); mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.target.nodeName === 'VIDEO') { processVideoElement(mutation.target); } mutation.addedNodes.forEach(node => { if (node.nodeName === 'VIDEO') processVideoElement(node); if (node.querySelectorAll) { node.querySelectorAll('video').forEach(processVideoElement); } }); }); }); _observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); """; scripts.add(UserScript( source: blobParserScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: false, )); scripts.add(UserScript( source: videoTagParserScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END, forMainFrameOnly: false, )); } await webviewController?.addUserScripts( userScripts: scripts, ); } @override Future unloadPage() async { await webviewController! .loadUrl(urlRequest: URLRequest(url: WebUri("about:blank"))); } @override void dispose() { headlessWebView?.dispose(); headlessWebView = null; webviewController = null; } } ================================================ FILE: lib/webview/video/impl/video_webview_impl.dart ================================================ import 'dart:async'; import 'dart:ui'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; import 'package:flutter_inappwebview_android/flutter_inappwebview_android.dart' as android_webview; class VideoWebviewImpl extends VideoWebviewController { PlatformHeadlessInAppWebView? headlessWebView; bool hasRegisteredHandlers = false; bool useLegacyParser = false; Timer? videoParserTimer; @override Future init() async { await _setupProxy(); headlessWebView ??= PlatformHeadlessInAppWebView( PlatformHeadlessInAppWebViewCreationParams( initialSize: const Size(360, 640), initialSettings: InAppWebViewSettings( userAgent: Utils.getRandomUA(), mediaPlaybackRequiresUserGesture: true, upgradeKnownHostsToHTTPS: false, mixedContentMode: MixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE, ), onWebViewCreated: (controller) { print('[WebView] Created (legacy fallback)'); webviewController = controller; initEventController.add(true); }, shouldInterceptRequest: (controller, request) async { if (useLegacyParser || isVideoSourceLoaded) return null; final url = request.url.toString(); final lower = url.toLowerCase(); if (_isAdUrl(lower)) return null; if (_isM3U8Url(lower) || _isRangeVideoRequest(lower, request.headers)) { logEventController .add('Native intercepted video URL: $url'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); unloadPage(); videoParserEventController.add((url, offset)); } return null; }, onLoadStart: (controller, url) async { logEventController.add('started loading: $url'); if (url.toString() != 'about:blank') { await _onLoadStart(); } }, onLoadStop: (controller, url) async { logEventController.add('loading completed: $url'); if (url.toString() != 'about:blank') { await _onLoadStop(); } }, onConsoleMessage: (controller, consoleMessage) { logEventController.add( 'Console [${consoleMessage.messageLevel}]: ${consoleMessage.message}'); }, onReceivedError: (controller, request, error) { logEventController.add( 'Error: ${error.description} - ${request.url}'); }, ), ); await headlessWebView?.run(); } @override Future loadUrl(String url, bool useLegacyParser, {int offset = 0}) async { await unloadPage(); if (!hasRegisteredHandlers) { _addJavaScriptHandlers(useLegacyParser); hasRegisteredHandlers = true; } count = 0; this.offset = offset; this.useLegacyParser = useLegacyParser; isIframeLoaded = false; isVideoSourceLoaded = false; videoLoadingEventController.add(true); await webviewController?.loadUrl(urlRequest: URLRequest(url: WebUri(url))); } void _addJavaScriptHandlers(bool useLegacyParser) { logEventController.add('Adding LogBridge handler'); webviewController?.addJavaScriptHandler( handlerName: 'LogBridge', callback: (args) { String message = args[0].toString(); if (message.contains('about:blank')) { return; } logEventController.add(message); }); if (useLegacyParser) { logEventController.add('Adding JSBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'JSBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); logEventController.add( 'If there is audio but no video, please report it to the rule developer.'); if ((message.contains('http') || message.startsWith('//')) && !message.contains('googleads') && !message.contains('googlesyndication.com') && !message.contains('prestrain.html') && !message.contains('prestrain%2Ehtml') && !message.contains('adtrafficquality')) { logEventController.add('Parsing video source $message'); String encodedUrl = Uri.encodeFull(message); if (Utils.decodeVideoSource(encodedUrl) != encodedUrl) { isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add( 'Loading video source ${Utils.decodeVideoSource(encodedUrl)}'); unloadPage(); videoParserEventController .add((Utils.decodeVideoSource(encodedUrl), offset)); } } }); } else { logEventController.add('Adding VideoBridgeDebug handler'); webviewController?.addJavaScriptHandler( handlerName: 'VideoBridgeDebug', callback: (args) { String message = args[0].toString(); logEventController.add('Callback received: $message'); if (message.contains('http') && !isVideoSourceLoaded) { logEventController.add('Loading video source: $message'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); unloadPage(); videoParserEventController.add((message, offset)); } }); } } Future _onLoadStart() async { if (!useLegacyParser) { logEventController.add('Injecting blob parser script (onLoadStart)'); await webviewController?.evaluateJavascript(source: """ try { window.flutter_inappwebview.callHandler('LogBridge', 'BlobParser script loaded: ' + window.location.href); } catch(e) {} const _r_text = window.Response.prototype.text; window.Response.prototype.text = function () { return new Promise((resolve, reject) => { _r_text.call(this).then((text) => { resolve(text); if (text.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + this.url); window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url); } }).catch(reject); }); } const _open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (content.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found: ' + args[1]); window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]); }; } catch {} }); return _open.apply(this, args); }; function injectIntoIframe(iframe) { try { const iframeWindow = iframe.contentWindow; if (!iframeWindow) return; const iframe_r_text = iframeWindow.Response.prototype.text; iframeWindow.Response.prototype.text = function () { return new Promise((resolve, reject) => { iframe_r_text.call(this).then((text) => { resolve(text); if (text.trim().startsWith("#EXTM3U")) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found in iframe: ' + this.url); window.flutter_inappwebview.callHandler('VideoBridgeDebug', this.url); } }).catch(reject); }); } const iframe_open = iframeWindow.XMLHttpRequest.prototype.open; iframeWindow.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (content.trim().startsWith("#EXTM3U") && args[1] !== null && args[1] !== undefined) { window.flutter_inappwebview.callHandler('LogBridge', 'M3U8 source found in iframe: ' + args[1]); window.flutter_inappwebview.callHandler('VideoBridgeDebug', args[1]); }; } catch {} }); return iframe_open.apply(this, args); } } catch (e) { console.error('iframe inject failed:', e); } } function setupIframeListeners() { document.querySelectorAll('iframe').forEach(iframe => { if (iframe.contentDocument) { injectIntoIframe(iframe); } iframe.addEventListener('load', () => injectIntoIframe(iframe)); }); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeName === 'IFRAME') { node.addEventListener('load', () => injectIntoIframe(node)); } if (node.querySelectorAll) { node.querySelectorAll('iframe').forEach(iframe => { iframe.addEventListener('load', () => injectIntoIframe(iframe)); }); } }); } }); }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupIframeListeners); } else { setupIframeListeners(); } """); } } Future _onLoadStop() async { if (!useLegacyParser) { logEventController.add('Injecting video tag parser script (onLoadStop)'); await webviewController?.evaluateJavascript(source: """ window.flutter_inappwebview.callHandler('LogBridge', 'VideoTagParser script loaded: ' + window.location.href); const _observer = new MutationObserver((mutations) => { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for video elements...'); for (const mutation of mutations) { if (mutation.type === "attributes" && mutation.target.nodeName === "VIDEO") { if (processVideoElement(mutation.target)) return; continue; } for (const node of mutation.addedNodes) { if (node.nodeName === "VIDEO") { if (processVideoElement(node)) return; } if (node.querySelectorAll) { for (const video of node.querySelectorAll("video")) { if (processVideoElement(video)) return; } } } } }); function processVideoElement(video) { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning video element for source URL'); let src = video.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { _observer.disconnect(); window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return true; } const sources = video.getElementsByTagName('source'); for (let source of sources) { src = source.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { _observer.disconnect(); window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return true; } } } function setupVideoProcessing() { for (const video of document.querySelectorAll("video")) { if (processVideoElement(video)) return; } _observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupVideoProcessing); } else { setupVideoProcessing(); } """); } if (useLegacyParser) { logEventController.add('Injecting JSBridgeDebug script (onLoadStop)'); await webviewController?.evaluateJavascript(source: """ window.flutter_inappwebview.callHandler('LogBridge', 'JSBridgeDebug script loaded: ' + window.location.href); function processIframeElement(iframe) { window.flutter_inappwebview.callHandler('LogBridge', 'Processing iframe element'); let src = iframe.getAttribute('src'); if (src) { window.flutter_inappwebview.callHandler('JSBridgeDebug', src); } } const _observer = new MutationObserver((mutations) => { window.flutter_inappwebview.callHandler('LogBridge', 'Scanning for iframes...'); mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.target.nodeName === 'IFRAME') { processIframeElement(mutation.target); } else { mutation.addedNodes.forEach(node => { if (node.nodeName === 'IFRAME') processIframeElement(node); if (node.querySelectorAll) { node.querySelectorAll('iframe').forEach(processIframeElement); } }); } }); }); _observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); """); } _startVideoParserTimer(); } void _startVideoParserTimer() { videoParserTimer?.cancel(); logEventController.add('Starting video parser timer'); videoParserTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (isVideoSourceLoaded) { timer.cancel(); return; } _pollVideoSource(); }); } Future _pollVideoSource() async { if (isVideoSourceLoaded) return; if (useLegacyParser) { await webviewController?.evaluateJavascript(source: """ (function() { var iframes = document.querySelectorAll('iframe'); window.flutter_inappwebview.callHandler('LogBridge', 'Timer scan: found ' + iframes.length + ' iframe(s)'); for (var i = 0; i < iframes.length; i++) { var src = iframes[i].getAttribute('src'); if (src) { window.flutter_inappwebview.callHandler('JSBridgeDebug', src); } } })(); """); } else { await webviewController?.evaluateJavascript(source: """ (function() { var videos = document.querySelectorAll('video'); window.flutter_inappwebview.callHandler('LogBridge', 'Timer scan: found ' + videos.length + ' video element(s)'); for (var i = 0; i < videos.length; i++) { var src = videos[i].getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found: ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return; } var sources = videos[i].getElementsByTagName('source'); for (var j = 0; j < sources.length; j++) { src = sources[j].getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.flutter_inappwebview.callHandler('LogBridge', 'VIDEO source found (source tag): ' + src); window.flutter_inappwebview.callHandler('VideoBridgeDebug', src); return; } } } })(); """); } } @override Future unloadPage() async { videoParserTimer?.cancel(); videoParserTimer = null; await webviewController ?.loadUrl(urlRequest: URLRequest(url: WebUri("about:blank"))); } @override void dispose() { videoParserTimer?.cancel(); videoParserTimer = null; headlessWebView?.dispose(); headlessWebView = null; webviewController = null; } bool _isM3U8Url(String lower) { final uri = Uri.tryParse(lower); if (uri == null) return false; return uri.path.endsWith('.m3u8'); } bool _isRangeVideoRequest(String lower, Map? headers) { if (headers == null) return false; final range = headers['Range'] ?? headers['range']; if (range == null || !range.startsWith('bytes=')) return false; if (lower.endsWith('.js') || lower.endsWith('.css') || lower.endsWith('.html') || lower.endsWith('.json') || lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.gif') || lower.endsWith('.svg') || lower.endsWith('.woff') || lower.endsWith('.woff2') || lower.endsWith('.wasm')) { return false; } return true; } bool _isAdUrl(String lower) { return lower.contains('googleads') || lower.contains('googlesyndication') || lower.contains('adtrafficquality') || lower.contains('doubleclick'); } Future _setupProxy() async { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) { return; } final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl); if (formattedProxy == null) { return; } try { final proxyAvailable = await android_webview.AndroidWebViewFeature.instance() .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE); if (!proxyAvailable) { KazumiLogger().w('WebView: 当前 Android 版本不支持代理'); return; } final proxyController = android_webview.AndroidProxyController.instance(); await proxyController.clearProxyOverride(); await proxyController.setProxyOverride( settings: ProxySettings( proxyRules: [ ProxyRule(url: formattedProxy), ], ), ); KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); } catch (e) { KazumiLogger().e('WebView: 设置代理失败 $e'); } } } ================================================ FILE: lib/webview/video/impl/video_webview_linux_impl.dart ================================================ import 'dart:async'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:kazumi/utils/utils.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; class VideoWebviewLinuxImpl extends VideoWebviewController { bool bridgeInited = false; @override Future init() async { final proxyConfig = _getProxyConfiguration(); webviewController ??= await WebviewWindow.create( configuration: CreateConfiguration( headless: true, proxy: proxyConfig, userScripts: const [ UserScript( source: blobScript, injectionTime: UserScriptInjectionTime.documentStart, forAllFrames: true), UserScript( source: iframeScript, injectionTime: UserScriptInjectionTime.documentEnd, forAllFrames: true), UserScript( source: videoScript, injectionTime: UserScriptInjectionTime.documentEnd, forAllFrames: true) ], ), ); bridgeInited = false; initEventController.add(true); } ProxyConfiguration? _getProxyConfiguration() { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) { return null; } final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final parsed = ProxyUtils.parseProxyUrl(proxyUrl); if (parsed == null) { return null; } final (host, port) = parsed; KazumiLogger().i('WebView: 代理设置成功 $host:$port'); return ProxyConfiguration(host: host, port: port); } Future initBridge(bool useLegacyParser) async { await initJSBridge(useLegacyParser); bridgeInited = true; } @override Future loadUrl(String url, bool useLegacyParser, {int offset = 0}) async { await unloadPage(); if (!bridgeInited) { await initBridge(useLegacyParser); } count = 0; this.offset = offset; isIframeLoaded = false; isVideoSourceLoaded = false; videoLoadingEventController.add(true); webviewController!.launch(url); } @override Future unloadPage() async { await redirect2Blank(); } @override void dispose() { webviewController!.close(); bridgeInited = false; } Future initJSBridge(bool useLegacyParser) async { webviewController!.addOnWebMessageReceivedCallback((message) async { if (message.contains('iframeMessage:')) { String messageItem = Uri.encodeFull(message.replaceFirst('iframeMessage:', '')); logEventController .add('Callback received: [iframe] ${Uri.decodeFull(messageItem)}'); if ((messageItem.contains('http') || messageItem.startsWith('//')) && !messageItem.contains('googleads') && !messageItem.contains('googlesyndication.com') && !messageItem.contains('prestrain.html') && !messageItem.contains('prestrain%2Ehtml') && !messageItem.contains('adtrafficquality')) { if (Utils.decodeVideoSource(messageItem) != Uri.encodeFull(messageItem) && useLegacyParser) { logEventController.add('Parsing video source $messageItem'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add( 'Loading video source ${Utils.decodeVideoSource(messageItem)}'); unloadPage(); videoParserEventController .add((Utils.decodeVideoSource(messageItem), offset)); } } } if (message.contains('videoMessage:')) { String messageItem = Uri.encodeFull(message.replaceFirst('videoMessage:', '')); logEventController .add('Callback received: [video] ${Uri.decodeFull(messageItem)}'); if (messageItem.contains('http')) { String videoUrl = Uri.decodeFull(messageItem); logEventController.add('Loading video source: $videoUrl'); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); unloadPage(); videoParserEventController.add((videoUrl, offset)); } } }); } static const String iframeScript = """ var iframes = document.getElementsByTagName('iframe'); for (var i = 0; i < iframes.length; i++) { var iframe = iframes[i]; var src = iframe.getAttribute('src'); if (src) { window.webkit.messageHandlers.msgToNative.postMessage('iframeMessage:' + src); } } """; static const String videoScript = """ function processVideoElement(video) { let src = video.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + src); return; } const sources = video.getElementsByTagName('source'); for (let source of sources) { src = source.getAttribute('src'); if (src && src.trim() !== '' && !src.startsWith('blob:') && !src.includes('googleads')) { window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + src); return; } } } document.querySelectorAll('video').forEach(processVideoElement); const _observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.target.nodeName === 'VIDEO') { processVideoElement(mutation.target); } mutation.addedNodes.forEach(node => { if (node.nodeName === 'VIDEO') processVideoElement(node); if (node.querySelectorAll) { node.querySelectorAll('video').forEach(processVideoElement); } }); }); }); _observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); """; static const String blobScript = """ const _r_text = window.Response.prototype.text; window.Response.prototype.text = function () { return new Promise((resolve, reject) => { _r_text.call(this).then((text) => { resolve(text); if (text.trim().startsWith("#EXTM3U")) { window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + this.url); } }).catch(reject); }); } const _open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("load", () => { try { let content = this.responseText; if (content.trim().startsWith("#EXTM3U")) { window.webkit.messageHandlers.msgToNative.postMessage('videoMessage:' + args[1]); }; } catch { } }); return _open.apply(this, args); } """; Future redirect2Blank() async { webviewController?.launch("about:blank"); } } ================================================ FILE: lib/webview/video/impl/video_webview_windows_impl.dart ================================================ import 'dart:async'; import 'package:webview_windows/webview_windows.dart'; import 'package:kazumi/webview/video/video_webview_controller.dart'; import 'package:kazumi/utils/storage.dart'; import 'package:kazumi/utils/proxy_utils.dart'; import 'package:kazumi/utils/logger.dart'; class VideoWebviewWindowsImpl extends VideoWebviewController { final List subscriptions = []; HeadlessWebview? headlessWebview; @override Future init() async { await _setupProxy(); headlessWebview ??= HeadlessWebview(); await headlessWebview!.run(); await headlessWebview!.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny); initEventController.add(true); } Future _setupProxy() async { final setting = GStorage.setting; final bool proxyEnable = setting.get(SettingBoxKey.proxyEnable, defaultValue: false); if (!proxyEnable) { return; } final String proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final formattedProxy = ProxyUtils.getFormattedProxyUrl(proxyUrl); if (formattedProxy == null) { return; } try { await WebviewController.initializeEnvironment( additionalArguments: '--proxy-server=$formattedProxy', ); KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); } catch (e) { KazumiLogger().e('WebView: 设置代理失败 $e'); } } @override Future loadUrl(String url, bool useLegacyParser, {int offset = 0}) async { await unloadPage(); count = 0; this.offset = offset; isIframeLoaded = false; isVideoSourceLoaded = false; videoLoadingEventController.add(true); subscriptions.add(headlessWebview!.onM3USourceLoaded.listen((data) { if (headlessWebview == null) return; String url = data['url'] ?? ''; if (url.isEmpty) { return; } unloadPage(); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add('Loading m3u8 source: $url'); videoParserEventController.add((url, offset)); })); subscriptions.add(headlessWebview!.onVideoSourceLoaded.listen((data) { if (headlessWebview == null) return; String url = data['url'] ?? ''; if (url.isEmpty) { return; } unloadPage(); isIframeLoaded = true; isVideoSourceLoaded = true; videoLoadingEventController.add(false); logEventController.add('Loading video source: $url'); videoParserEventController.add((url, offset)); })); await headlessWebview!.loadUrl(url); } @override Future unloadPage() async { subscriptions.forEach((s) { try { s.cancel(); } catch (_) {} }); subscriptions.clear(); await redirect2Blank(); } @override void dispose() { subscriptions.forEach((s) { try { s.cancel(); } catch (_) {} }); subscriptions.clear(); headlessWebview?.dispose(); headlessWebview = null; } // The webview_windows package does not have a method to unload the current page. // The loadUrl method opens a new tab, which can lead to memory leaks. // Directly disposing of the webview controller would require reinitialization when switching episodes, which is costly. // Therefore, this method is used to redirect to a blank page instead. Future redirect2Blank() async { if (headlessWebview == null) return; try { await headlessWebview!.executeScript(''' window.location.href = 'about:blank'; '''); } catch (e) { KazumiLogger().d('WebView: redirect2Blank skipped (likely disposed): $e'); } } } ================================================ FILE: lib/webview/video/video_webview_controller.dart ================================================ import 'dart:io'; import 'dart:async'; import 'package:kazumi/webview/video/impl/video_webview_android_impl.dart'; import 'package:kazumi/webview/video/impl/video_webview_impl.dart'; import 'package:kazumi/webview/video/impl/video_webview_windows_impl.dart'; import 'package:kazumi/webview/video/impl/video_webview_linux_impl.dart'; import 'package:kazumi/webview/video/impl/video_webview_apple_impl.dart'; import 'package:kazumi/utils/utils.dart'; abstract class VideoWebviewController { // Webview controller T? webviewController; // Retry count int count = 0; // Last watched position int offset = 0; bool isIframeLoaded = false; bool isVideoSourceLoaded = false; /// Webview initialization method Future init(); final StreamController initEventController = StreamController.broadcast(); // Stream to notify when the webview is initialized Stream get onInitialized => initEventController.stream; final StreamController logEventController = StreamController.broadcast(); // Stream to subscribe to webview logs Stream get onLog => logEventController.stream; final StreamController videoLoadingEventController = StreamController.broadcast(); // Stream to notify when the video source is loaded Stream get onVideoLoading => videoLoadingEventController.stream; // Stream to notify video source URL when the video source is loaded // The first parameter is the video source URL and the second parameter is the video offset (start position) final StreamController<(String, int)> videoParserEventController = StreamController<(String, int)>.broadcast(); Stream<(String, int)> get onVideoURLParser => videoParserEventController.stream; /// Webview load URL method Future loadUrl(String url, bool useLegacyParser, {int offset = 0}); /// Webview unload page method Future unloadPage(); /// Webview dispose method void dispose(); } class VideoWebviewControllerFactory { static VideoWebviewController getController() { if (Platform.isWindows) { return VideoWebviewWindowsImpl(); } if (Platform.isLinux) { return VideoWebviewLinuxImpl(); } if (Platform.isMacOS || Platform.isIOS) { return VideoWebviewAppleImpl(); } if (Platform.isAndroid && Utils.isDocumentStartScriptSupported) { return VideoWebviewAndroidImpl(); } return VideoWebviewImpl(); } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "kazumi") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "io.github.Predidit.Kazumi") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # add runpath, shared libs of a release bundle is in lib dir, plugin must add $ORIGIN to runpath to find libmpv # set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\$ORIGIN") # set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\$ORIGIN") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include #include #include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin"); flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); g_autoptr(FlPluginRegistrar) media_kit_video_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); g_autoptr(FlPluginRegistrar) tray_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); tray_manager_plugin_register_with_registrar(tray_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); } ================================================ FILE: linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window dynamic_color flutter_volume_controller media_kit_libs_linux media_kit_video screen_retriever_linux tray_manager url_launcher_linux window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/my_application.cc ================================================ #include "my_application.h" #include #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; FlMethodChannel* intent_method_channel; FlMethodChannel* storage_method_channel; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) static FlMethodResponse* is_running_on_x11() { GdkDisplay* display = gdk_display_get_default(); gboolean is_x11 = GDK_IS_X11_DISPLAY(display); g_autoptr(FlValue) result = fl_value_new_bool(is_x11); return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } static void storage_method_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { g_autoptr(FlMethodResponse) response = nullptr; if (strcmp(fl_method_call_get_name(method_call), "getAvailableStorage") == 0) { const gchar* path = "/"; FlValue* args = fl_method_call_get_args(method_call); if (fl_value_get_type(args) == FL_VALUE_TYPE_MAP) { FlValue* path_value = fl_value_lookup_string(args, "path"); if (path_value != nullptr && fl_value_get_type(path_value) == FL_VALUE_TYPE_STRING) { path = fl_value_get_string(path_value); } } struct statvfs stat; if (statvfs(path, &stat) == 0) { gint64 available = (gint64)stat.f_bavail * (gint64)stat.f_frsize; g_autoptr(FlValue) result = fl_value_new_int(available); response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } else { g_autoptr(FlValue) result = fl_value_new_int(-1); response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } g_autoptr(GError) error = nullptr; if (!fl_method_call_respond(method_call, response, &error)) { g_warning("Failed to send response: %s", error->message); } } static void intent_method_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { g_autoptr(FlMethodResponse) response = nullptr; if (strcmp(fl_method_call_get_name(method_call), "isRunningOnX11") == 0) { response = is_running_on_x11(); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } g_autoptr(GError) error = nullptr; if (!fl_method_call_respond(method_call, response, &error)) { g_warning("Failed to send response: %s", error->message); } } // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "kazumi"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "kazumi"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); self->intent_method_channel = fl_method_channel_new( fl_engine_get_binary_messenger(fl_view_get_engine(view)), "com.predidit.kazumi/intent", FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler( self->intent_method_channel, intent_method_call_handler, self, nullptr); self->storage_method_channel = fl_method_channel_new( fl_engine_get_binary_messenger(fl_view_get_engine(view)), "com.predidit.kazumi/storage", FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler( self->storage_method_channel, storage_method_call_handler, self, nullptr); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GApplication::startup. static void my_application_startup(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } // Implements GApplication::shutdown. static void my_application_shutdown(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application shutdown. G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); g_clear_object(&self->intent_method_channel); g_clear_object(&self->storage_method_channel); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->startup = my_application_startup; G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import connectivity_plus import dynamic_color import flutter_inappwebview_macos import flutter_volume_controller import media_kit_libs_macos_video import media_kit_video import package_info_plus import path_provider_foundation import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin import tray_manager import url_launcher_macos import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS import SwiftUI import AVKit @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } var playerView: AVPlayerView! var player: AVPlayer? var videoUrl: URL? var httpReferer: String = "" var menuChannel: FlutterMethodChannel? override func applicationDidFinishLaunching(_ notification: Notification) { setMenuEnabled(menu: "Player", enable: false) let controller : FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController let channel = FlutterMethodChannel.init(name: "com.predidit.kazumi/intent", binaryMessenger: controller.engine.binaryMessenger) self.menuChannel = FlutterMethodChannel.init(name: "com.predidit.kazumi/appmenu",binaryMessenger: controller.engine.binaryMessenger) channel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in if call.method == "openWithReferer" { guard let args = call.arguments else { return } if let myArgs = args as? [String: Any], let url = myArgs["url"] as? String, let referer = myArgs["referer"] as? String { self.openVideoWithReferer(url: url, referer: referer) } result(nil) } else { result(FlutterMethodNotImplemented) } }); let storageChannel = FlutterMethodChannel.init(name: "com.predidit.kazumi/storage", binaryMessenger: controller.engine.binaryMessenger) storageChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in if call.method == "getAvailableStorage" { do { let attrs = try FileManager.default.attributesOfFileSystem( forPath: NSHomeDirectory() ) if let freeSize = attrs[.systemFreeSize] as? Int64 { result(freeSize) } else { result(-1) } } catch { result(-1) } } else { result(FlutterMethodNotImplemented) } }); self.menuChannel?.setMethodCallHandler({call,result in switch call.method { case "setMenuEnabled": guard let args = call.arguments as? [String: Any], let menu = args["menu"] as? String, let enable = args["enable"] as? Bool else { result(FlutterMethodNotImplemented) return } self.setMenuEnabled(menu: menu, enable: enable) result(nil) default: result(FlutterMethodNotImplemented) } }); } func findApplicationsByMimeType() -> [URL] { let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.mp4") FileManager.default.createFile(atPath: tempFileURL.path, contents: nil, attributes: nil) if #available(macOS 12.0, *) { let listOfExternalApps = NSWorkspace.shared.urlsForApplications(toOpen: tempFileURL) if FileManager.default.fileExists(atPath: tempFileURL.path) { do { try FileManager.default.removeItem(atPath: tempFileURL.path) } catch { print("Delete error: \(error.localizedDescription)") } } return listOfExternalApps } else { if FileManager.default.fileExists(atPath: tempFileURL.path) { do { try FileManager.default.removeItem(atPath: tempFileURL.path) } catch { print("Delete error: \(error.localizedDescription)") } } return [] } } private func openVideoWithReferer(url: String, referer: String) { videoUrl = URL(string: url) httpReferer = referer let selectMenu = NSMenu() let appLists = findApplicationsByMimeType() /* AVPlayer menu item start */ let menuItem = NSMenuItem() menuItem.attributedTitle = NSAttributedString(string: "AVPlayer", attributes: [.font: NSFont.systemFont(ofSize: 14)]) menuItem.action = #selector(openWithAVPlayer) let icon = NSWorkspace.shared.icon(forFile: "/System/Applications/Preview.app") icon.size = NSSize(width: 16, height: 16) menuItem.image = icon selectMenu.addItem(menuItem) /* AVPlayer menu item end */ /* Applications menu item start */ for appList in appLists { let appBundle = Bundle(url: appList) let appName = appBundle?.infoDictionary?["CFBundleName"] as? String ?? "" if appName == "QuickTime Player" || appName == "Books" { continue } let menuItem = NSMenuItem() menuItem.attributedTitle = NSAttributedString(string: "\(appName).app", attributes: [.font: NSFont.systemFont(ofSize: 14)]) if appName == "VLC" { menuItem.action = #selector(openWithVLC(_:)) } else { menuItem.action = #selector(openWithSelectedApp(_:)) } menuItem.representedObject = "/Applications/\(appName).app/Contents/MacOS/\(appName)" let icon = NSWorkspace.shared.icon(forFile: "/Applications/\(appName).app") icon.size = NSSize(width: 16, height: 16) menuItem.image = icon selectMenu.addItem(menuItem) } /* Applications menu item end */ selectMenu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) } @objc func openWithAVPlayer () { let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 1280, height: 860), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false) window.center() window.makeKeyAndOrderFront(nil) window.isReleasedWhenClosed = false playerView = AVPlayerView(frame: window.contentView!.bounds) playerView.autoresizingMask = [.width, .height] window.contentView?.addSubview(playerView) window.delegate = self let headers: [String: String] = [ "Referer": httpReferer, ] let asset = AVURLAsset(url: videoUrl!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) let playerItem = AVPlayerItem(asset: asset) player = AVPlayer(playerItem: playerItem) playerView.player = player playerView.player?.play() } @objc func openWithSelectedApp (_ sender: NSMenuItem) { if !httpReferer.isEmpty { let alert = NSAlert() alert.messageText = "打开应用失败" alert.informativeText = "该应用不支持 Referer 请求头,打开失败。请使用 AVPlayer/VLC 打开或更换规则。" alert.runModal() return } if let selectedApp = sender.representedObject { let process = Process() process.launchPath = selectedApp as? String process.arguments = [videoUrl!.absoluteString] do { try process.run() } catch { print("Failed to open app: \(error)") } } } @objc func openWithVLC (_ sender: NSMenuItem) { if let selectedApp = sender.representedObject { let process = Process() process.launchPath = selectedApp as? String process.arguments = [videoUrl!.absoluteString, ":http-referrer=" + httpReferer] do { try process.run() } catch { print("Failed to open app: \(error)") } } } var isPlayerActive: Bool = false func sendToFlutter(_ command: String){ menuChannel?.invokeMethod(command, arguments: nil) } @IBAction func menuPlayPause(_ sender: Any) { sendToFlutter("playorpause") } @IBAction func menuNext(_ sender: Any) { sendToFlutter("next") } @IBAction func menuPrevious(_ sender: Any) { sendToFlutter("prev") } @IBAction func menuForward(_ sender: Any) { sendToFlutter("forward") } @IBAction func menuRewind(_ sender: Any) { sendToFlutter("rewind") } @IBAction func menuVolumeUp(_ sender: Any) { sendToFlutter("volumeup") } @IBAction func menuVolumeDown(_ sender: Any) { sendToFlutter("volumedown") } @IBAction func menuToggleMute(_ sender: Any) { sendToFlutter("togglemute") } @IBAction func menuToggleDanmaku(_ sender: Any) { sendToFlutter("toggledanmaku") } @IBAction func menuSkip(_ sender: Any) { sendToFlutter("skip") } @IBAction func menuSpeed1(_ sender: Any) { sendToFlutter("speed1") } @IBAction func menuSpeed2(_ sender: Any) { sendToFlutter("speed2") } @IBAction func menuSpeed3(_ sender: Any) { sendToFlutter("speed3") } @IBAction func menuSpeedUp(_ sender: Any) { sendToFlutter("speedup") } @IBAction func menuSpeedDown(_ sender: Any) { sendToFlutter("speeddown") } func setMenuEnabled(menu: String, enable: Bool) { if let menuItem = NSApp.mainMenu?.items.first(where: { $0.identifier?.rawValue == menu }) { menuItem.isEnabled = enable } } } extension AppDelegate: NSWindowDelegate { func windowWillClose(_ notification: Notification) { player?.pause() player = nil } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "info": { "version": 1, "author": "xcode" }, "images": [ { "size": "16x16", "idiom": "mac", "filename": "app_icon_16.png", "scale": "1x" }, { "size": "16x16", "idiom": "mac", "filename": "app_icon_32.png", "scale": "2x" }, { "size": "32x32", "idiom": "mac", "filename": "app_icon_32.png", "scale": "1x" }, { "size": "32x32", "idiom": "mac", "filename": "app_icon_64.png", "scale": "2x" }, { "size": "128x128", "idiom": "mac", "filename": "app_icon_128.png", "scale": "1x" }, { "size": "128x128", "idiom": "mac", "filename": "app_icon_256.png", "scale": "2x" }, { "size": "256x256", "idiom": "mac", "filename": "app_icon_256.png", "scale": "1x" }, { "size": "256x256", "idiom": "mac", "filename": "app_icon_512.png", "scale": "2x" }, { "size": "512x512", "idiom": "mac", "filename": "app_icon_512.png", "scale": "1x" }, { "size": "512x512", "idiom": "mac", "filename": "app_icon_1024.png", "scale": "2x" } ] } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = kazumi // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server com.apple.security.network.client com.apple.security.files.downloads.read-write ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSAppTransportSecurity NSAllowsArbitraryLoads NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS import window_manager class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() self.backgroundColor = NSColor.clear flutterViewController.backgroundColor = NSColor.clear let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { super.order(place, relativeTo: otherWin) hiddenWindowAtLaunch() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client com.apple.security.network.server com.apple.security.files.downloads.read-write ================================================ FILE: macos/Runner/en-GB.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "APP_NAME"; ObjectID = "1Xt-HY-uBw"; */ "1Xt-HY-uBw.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ "2oI-Rn-ZJC.title" = "Transformations"; /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ "3rS-ZA-NoH.title" = "Speech"; /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ "4J7-dP-txa.title" = "Enter Full Screen"; /* Class = "NSMenuItem"; title = "Quit APP_NAME"; ObjectID = "4sb-4s-VLi"; */ "4sb-4s-VLi.title" = "Quit APP_NAME"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ "5QF-Oa-p0T.title" = "Edit"; /* Class = "NSMenuItem"; title = "About APP_NAME"; ObjectID = "5kV-Vb-QxS"; */ "5kV-Vb-QxS.title" = "About APP_NAME"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ "6dh-zS-Vam.title" = "Redo"; /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ "AYu-sK-qS6.title" = "Main Menu"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "EPT-qC-fAb"; */ "EPT-qC-fAb.title" = "Help"; /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ "H8h-7b-M4v.title" = "View"; /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ "HyV-fh-RgO.title" = "View"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ "Kd2-mp-pUS.title" = "Show All"; /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ "LE2-aR-0XJ.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ "NMo-om-nkz.title" = "Services"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ "OY7-WF-poV.title" = "Minimize"; /* Class = "NSMenuItem"; title = "Hide APP_NAME"; ObjectID = "Olw-nP-bQN"; */ "Olw-nP-bQN.title" = "Hide APP_NAME"; /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ "Oyz-dy-DGm.title" = "Stop Speaking"; /* Class = "NSWindow"; title = "APP_NAME"; ObjectID = "QvC-M9-y7g"; */ "QvC-M9-y7g.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ "R4o-n2-Eq4.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ "Ruw-6m-B2m.title" = "Select All"; /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ "Td7-aD-5lo.title" = "Window"; /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ "UEZ-Bs-lqG.title" = "Capitalize"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ "Vdr-fp-XzO.title" = "Hide Others"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ "W48-6f-4Dl.title" = "Edit"; /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ "Ynk-f8-cLZ.title" = "Start Speaking"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ "aUF-d1-5bR.title" = "Window"; /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ "c8a-y6-VQd.title" = "Transformations"; /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ "d9M-CD-aMd.title" = "Make Lower Case"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ "dRJ-4n-Yzg.title" = "Undo"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ "gVA-U4-sdL.title" = "Paste"; /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ "hz9-B4-Xy5.title" = "Services"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ "pa3-QI-u2k.title" = "Delete"; /* Class = "NSMenu"; title = "Help"; ObjectID = "rJ0-wn-3NY"; */ "rJ0-wn-3NY.title" = "Help"; /* Class = "NSMenu"; title = "APP_NAME"; ObjectID = "uQy-DD-JDr"; */ "uQy-DD-JDr.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ "uRl-iY-unG.title" = "Cut"; /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ "vmV-6d-7jI.title" = "Make Upper Case"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ "x3v-GG-iWU.title" = "Copy"; /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ "xrE-MZ-jX0.title" = "Speech"; ================================================ FILE: macos/Runner/en.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "APP_NAME"; ObjectID = "1Xt-HY-uBw"; */ "1Xt-HY-uBw.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ "2oI-Rn-ZJC.title" = "Transformations"; /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ "3rS-ZA-NoH.title" = "Speech"; /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ "4J7-dP-txa.title" = "Enter Full Screen"; /* Class = "NSMenuItem"; title = "Quit APP_NAME"; ObjectID = "4sb-4s-VLi"; */ "4sb-4s-VLi.title" = "Quit APP_NAME"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ "5QF-Oa-p0T.title" = "Edit"; /* Class = "NSMenuItem"; title = "About APP_NAME"; ObjectID = "5kV-Vb-QxS"; */ "5kV-Vb-QxS.title" = "About APP_NAME"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ "6dh-zS-Vam.title" = "Redo"; /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ "AYu-sK-qS6.title" = "Main Menu"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "EPT-qC-fAb"; */ "EPT-qC-fAb.title" = "Help"; /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ "H8h-7b-M4v.title" = "View"; /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ "HyV-fh-RgO.title" = "View"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ "Kd2-mp-pUS.title" = "Show All"; /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ "LE2-aR-0XJ.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ "NMo-om-nkz.title" = "Services"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ "OY7-WF-poV.title" = "Minimize"; /* Class = "NSMenuItem"; title = "Hide APP_NAME"; ObjectID = "Olw-nP-bQN"; */ "Olw-nP-bQN.title" = "Hide APP_NAME"; /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ "Oyz-dy-DGm.title" = "Stop Speaking"; /* Class = "NSWindow"; title = "APP_NAME"; ObjectID = "QvC-M9-y7g"; */ "QvC-M9-y7g.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ "R4o-n2-Eq4.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ "Ruw-6m-B2m.title" = "Select All"; /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ "Td7-aD-5lo.title" = "Window"; /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ "UEZ-Bs-lqG.title" = "Capitalize"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ "Vdr-fp-XzO.title" = "Hide Others"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ "W48-6f-4Dl.title" = "Edit"; /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ "Ynk-f8-cLZ.title" = "Start Speaking"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ "aUF-d1-5bR.title" = "Window"; /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ "c8a-y6-VQd.title" = "Transformations"; /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ "d9M-CD-aMd.title" = "Make Lower Case"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ "dRJ-4n-Yzg.title" = "Undo"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ "gVA-U4-sdL.title" = "Paste"; /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ "hz9-B4-Xy5.title" = "Services"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ "pa3-QI-u2k.title" = "Delete"; /* Class = "NSMenu"; title = "Help"; ObjectID = "rJ0-wn-3NY"; */ "rJ0-wn-3NY.title" = "Help"; /* Class = "NSMenu"; title = "APP_NAME"; ObjectID = "uQy-DD-JDr"; */ "uQy-DD-JDr.title" = "APP_NAME"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ "uRl-iY-unG.title" = "Cut"; /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ "vmV-6d-7jI.title" = "Make Upper Case"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ "x3v-GG-iWU.title" = "Copy"; /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ "xrE-MZ-jX0.title" = "Speech"; ================================================ FILE: macos/Runner/zh-Hans.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Toggle Mute"; ObjectID = "0go-7w-Agj"; */ "0go-7w-Agj.title" = "切换静音"; /* Class = "NSMenuItem"; title = "Forward"; ObjectID = "1oV-tb-v1I"; */ "1oV-tb-v1I.title" = "快进"; /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ "2oI-Rn-ZJC.title" = "转换"; /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ "3rS-ZA-NoH.title" = "语音"; /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ "4J7-dP-txa.title" = "进入全屏幕"; /* Class = "NSMenuItem"; title = "Quit APP_NAME"; ObjectID = "4sb-4s-VLi"; */ "4sb-4s-VLi.title" = "退出APP_NAME"; /* Class = "NSMenuItem"; title = "About APP_NAME"; ObjectID = "5kV-Vb-QxS"; */ "5kV-Vb-QxS.title" = "关于APP_NAME"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ "5QF-Oa-p0T.title" = "编辑"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ "6dh-zS-Vam.title" = "重做"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ "aUF-d1-5bR.title" = "窗口"; /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ "c8a-y6-VQd.title" = "转换"; /* Class = "NSMenuItem"; title = "Volume Down"; ObjectID = "CEJ-Pz-JnN"; */ "CEJ-Pz-JnN.title" = "音量减"; /* Class = "NSMenuItem"; title = "Next"; ObjectID = "cVr-GM-2GR"; */ "cVr-GM-2GR.title" = "下一集"; /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ "d9M-CD-aMd.title" = "转换为小写"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ "dRJ-4n-Yzg.title" = "撤销"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "EPT-qC-fAb"; */ "EPT-qC-fAb.title" = "帮助"; /* Class = "NSMenu"; title = "Speed"; ObjectID = "FYI-7j-0Xf"; */ "FYI-7j-0Xf.title" = "倍速"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ "gVA-U4-sdL.title" = "粘贴"; /* Class = "NSMenuItem"; title = "Rewind"; ObjectID = "hfw-bJ-3tF"; */ "hfw-bJ-3tF.title" = "快退"; /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ "hz9-B4-Xy5.title" = "服务"; /* Class = "NSMenuItem"; title = "Skip"; ObjectID = "jFI-nV-KPO"; */ "jFI-nV-KPO.title" = "跳过"; /* Class = "NSMenuItem"; title = "Speed Up"; ObjectID = "kCQ-C0-LpW"; */ "kCQ-C0-LpW.title" = "倍速加"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ "Kd2-mp-pUS.title" = "显示全部"; /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ "LE2-aR-0XJ.title" = "前置全部窗口"; /* Class = "NSMenuItem"; title = "Toggle Danmaku"; ObjectID = "lmU-yX-0QG"; */ "lmU-yX-0QG.title" = "弹幕开关"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ "NMo-om-nkz.title" = "服务"; /* Class = "NSMenuItem"; title = "Volume Up"; ObjectID = "o2L-YK-Znd"; */ "o2L-YK-Znd.title" = "音量加"; /* Class = "NSMenuItem"; title = "Hide APP_NAME"; ObjectID = "Olw-nP-bQN"; */ "Olw-nP-bQN.title" = "隐藏APP_NAME"; /* Class = "NSMenuItem"; title = "Previous"; ObjectID = "Ox4-Od-VDn"; */ "Ox4-Od-VDn.title" = "上一集"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ "OY7-WF-poV.title" = "最小化"; /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ "Oyz-dy-DGm.title" = "停止讲话"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ "pa3-QI-u2k.title" = "删除"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ "R4o-n2-Eq4.title" = "缩放"; /* Class = "NSMenu"; title = "Help"; ObjectID = "rJ0-wn-3NY"; */ "rJ0-wn-3NY.title" = "帮助"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ "Ruw-6m-B2m.title" = "全选"; /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ "Td7-aD-5lo.title" = "窗口"; /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ "UEZ-Bs-lqG.title" = "大写"; /* Class = "NSMenu"; title = "Player"; ObjectID = "ufy-gc-bv9"; */ "ufy-gc-bv9.title" = "播放器"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ "uRl-iY-unG.title" = "剪切"; /* Class = "NSMenuItem"; title = "Speed Down"; ObjectID = "V9W-wp-A8v"; */ "V9W-wp-A8v.title" = "倍速减"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ "Vdr-fp-XzO.title" = "隐藏其他应用"; /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ "vmV-6d-7jI.title" = "转换为大写"; /* Class = "NSMenuItem"; title = "Player"; ObjectID = "vQb-zP-A4Q"; */ "vQb-zP-A4Q.title" = "播放器"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ "W48-6f-4Dl.title" = "编辑"; /* Class = "NSMenuItem"; title = "Speed"; ObjectID = "weg-Na-dVX"; */ "weg-Na-dVX.title" = "倍速"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ "x3v-GG-iWU.title" = "复制"; /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ "xrE-MZ-jX0.title" = "语音"; /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ "Ynk-f8-cLZ.title" = "开始讲话"; /* Class = "NSMenuItem"; title = "Play / Pause"; ObjectID = "Yz8-Qq-Cit"; */ "Yz8-Qq-Cit.title" = "播放 / 暂停"; ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 08D6925B2E3A29C100F22E48 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = ""; }; 08D6925D2E3A29CC00F22E48 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; 08D6925F2E3A29CE00F22E48 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/MainMenu.strings"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* Kazumi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Kazumi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* Kazumi.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* Kazumi.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = "zh-Hans"; hasScannedForEncodings = 0; knownRegions = ( Base, en, "zh-Hans", "en-GB", ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, 08D6925B2E3A29C100F22E48 /* zh-Hans */, 08D6925D2E3A29CC00F22E48 /* en */, 08D6925F2E3A29CE00F22E48 /* en-GB */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.kazumi.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/kazumi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/kazumi"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PRODUCT_NAME = Kazumi; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PRODUCT_NAME = Kazumi; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PRODUCT_NAME = Kazumi; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, 331C80DD294CF71000263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/RunnerTests/RunnerTests.swift ================================================ import FlutterMacOS import Cocoa import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: pubspec.yaml ================================================ name: kazumi description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 2.0.5+20005 environment: sdk: ">=3.3.4 <4.0.0" flutter: 3.41.5 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 flutter_modular: ^6.3.4 mobx: ^2.6.0 flutter_mobx: ^2.3.0 dio: ^5.0.0 cookie_jar: ^4.0.9 connectivity_plus: ^6.0.5 path_provider: ^2.1.5 hive_ce: ^2.16.0 hive_ce_flutter: ^2.3.3 cached_network_image: ^3.4.1 card_settings_ui: ^2.0.1 # fvp: ^0.28.0 # video_player: ^2.9.1 flutter_volume_controller: ^1.3.2 audio_video_progress_bar: ^2.0.2 dynamic_color: ^1.8.1 provider: ^6.1.2 flutter_displaymode: ^0.6.0 url_launcher: ^6.3.0 window_manager: ^0.5.1 xpath_selector: ^3.0.2 xpath_selector_html_parser: ^3.0.1 canvas_danmaku: ^0.3.1 webdav_client: ^1.2.2 tray_manager: ^0.5.0 dlna_dart: ^0.0.8 logger: ^2.6.2 flutter_rating_bar: ^4.0.1 scrollview_observer: ^1.22.0 saver_gallery: ^4.1.0 screen_brightness_android: ^2.1.3 screen_brightness_ios: ^2.1.2 screen_brightness_ohos: ^2.1.2 screen_brightness_platform_interface: ^2.1.0 synchronized: any flutter_svg: ^2.2.3 antlr4: ^4.13.2 fl_chart: ^1.1.1 flutter_foreground_task: ^9.0.0 skeletonizer: ^2.1.2 flutter_inappwebview_platform_interface: ^1.3.0+1 flutter_inappwebview_ios: ^1.1.2 flutter_inappwebview_macos: ^1.1.2 flutter_inappwebview_android: ^1.1.3 upgrader: ^12.3.0 open_filex: ^4.7.0 html: any material_color_utilities: any path: any webview_windows: git: url: https://github.com/Predidit/flutter-webview-windows.git ref: 1b9e83371356c04d8b0fb7ab1d20ebacf5cf9764 desktop_webview_window: git: url: https://github.com/Predidit/linux_webview_window.git ref: 297b39532c426263d2fa9fde4d70d2b5bdfc8059 media_kit: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./media_kit media_kit_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./media_kit_video media_kit_libs_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/universal/media_kit_libs_video dependency_overrides: media_kit_libs_linux: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/linux/media_kit_libs_linux media_kit_libs_ios_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/ios/media_kit_libs_ios_video media_kit_libs_android_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/android/media_kit_libs_android_video media_kit_libs_windows_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/windows/media_kit_libs_windows_video media_kit_libs_macos_video: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/macos/media_kit_libs_macos_video media_kit_libs_ohos: git: url: https://github.com/Predidit/media-kit.git ref: 4cd7c29566da395229c398d2ec4d0ef96b5e8970 path: ./libs/ohos/media_kit_libs_ohos dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 build_runner: ^2.10.4 mobx_codegen: ^2.7.5 hive_ce_generator: ^1.10.0 flutter_launcher_icons: ^0.14.3 flutter_native_splash: ^2.4.3 msix: ^3.16.12 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec flutter_launcher_icons: android: true ios: true remove_alpha_ios: true image_path: assets/images/logo/logo_android.png image_path_android: assets/images/logo/logo_android.png image_path_ios: assets/images/logo/logo_ios.png adaptive_icon_background: "#ffffff" adaptive_icon_foreground: assets/images/logo/logo_android.png adaptive_icon_monochrome: assets/images/logo/logo_android.png macos: generate: true image_path: assets/images/logo/logo_rounded.png windows: generate: true image_path: assets/images/logo/logo_rounded.png icon_size: 256 # min:48, max:256, default: 48 flutter_native_splash: android: false ios: true web: false color_ios: "#ffffff" color_dark_ios: "#212121" image_ios: assets/images/logo/logo_ios.png image_dark_ios: assets/images/logo/logo_ios.png msix_config: display_name: Kazumi publisher: CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US logo_path: assets/images/logo/logo_rounded.png sign_msix: false install_certificate: false build_windows: false # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg assets: - assets/images/ - assets/plugins/ - assets/shaders/ - assets/images/logo/ - assets/statements/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages fonts: - family: MI_Sans_Regular fonts: - asset: assets/fonts/MiSans-Regular.ttf ================================================ FILE: test/m3u8_parser_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:kazumi/utils/m3u8_parser.dart'; void main() { group('M3U8 Parser', () { // ── Master playlist ────────────────────────────────────────────────────── test('Test 1: Mux master playlist', () { const content = r''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:BANDWIDTH=246440,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=320x184 url_2/193039199_mp4_h264_aac_ld_2.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=460560,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=512x288 url_4/193039199_mp4_h264_aac_sd_4.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=836280,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=848x480 url_6/193039199_mp4_h264_aac_480p_6.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2149280,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720 url_0/193039199_mp4_h264_aac_hd_7.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6221600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080 url_8/193039199_mp4_h264_aac_fhd_8.m3u8 '''; const baseUrl = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'; expect(M3u8Parser.detectType(content), M3u8Type.master); final master = M3u8Parser.parseMasterPlaylist(content, baseUrl); expect(master.variants.length, 5); final best = master.bestVariant; expect(best.bandwidth, 6221600); expect(best.resolution, '1920x1080'); expect(best.uri, 'https://test-streams.mux.dev/x36xhzz/url_8/193039199_mp4_h264_aac_fhd_8.m3u8'); }); // ── Media playlist with VOD + ENDLIST ──────────────────────────────────── test('Test 2: Mux media playlist (PLAYLIST-TYPE:VOD + ENDLIST) -> isVod=true', () { const content = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, seg_00000.ts #EXTINF:10.0, seg_00001.ts #EXTINF:10.0, seg_00002.ts #EXTINF:9.6, seg_00003.ts #EXT-X-ENDLIST '''; const baseUrl = 'https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8'; expect(M3u8Parser.detectType(content), M3u8Type.media); final playlist = M3u8Parser.parseMediaPlaylist(content, baseUrl); expect(playlist.isVod, isTrue); expect(playlist.targetDuration, 10.0); expect(playlist.segments.length, 4); expect(playlist.segments.first.uri, 'https://test-streams.mux.dev/x36xhzz/url_0/seg_00000.ts'); expect(content.contains('#EXT-X-PLAYLIST-TYPE:VOD'), isTrue); expect(content.contains('#EXT-X-ENDLIST'), isTrue); }); // ── Apple-style: VOD tag present, no ENDLIST ───────────────────────────── test('Test 3: Apple media playlist (PLAYLIST-TYPE:VOD, no ENDLIST) -> isVod=true', () { const content = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:8 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:7.975, fileSequence0.mp4 #EXTINF:7.941, fileSequence1.mp4 #EXTINF:7.975, fileSequence2.mp4 '''; const baseUrl = 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/v5/prog_index.m3u8'; final playlist = M3u8Parser.parseMediaPlaylist(content, baseUrl); expect(content.contains('#EXT-X-PLAYLIST-TYPE:VOD'), isTrue); expect(content.contains('#EXT-X-ENDLIST'), isFalse); expect(playlist.isVod, isTrue); expect(playlist.segments.length, 3); }); // ── No VOD tag, no ENDLIST ─────────────────────────────────────────────── test('Test 4: no VOD tag no ENDLIST -> fallback isVod=true', () { const content = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, https://example.com/seg_00000.ts #EXTINF:10.0, https://example.com/seg_00001.ts #EXTINF:8.5, https://example.com/seg_00002.ts '''; final playlist = M3u8Parser.parseMediaPlaylist(content, 'https://example.com/playlist.m3u8'); expect(playlist.isVod, isTrue); expect(playlist.segments.length, 3); }); // ── EVENT playlist without ENDLIST → not VOD ──────────────────────────── test('Test 5: PLAYLIST-TYPE:EVENT no ENDLIST -> isVod=false', () { const content = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:EVENT #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, https://example.com/seg_00000.ts #EXTINF:10.0, https://example.com/seg_00001.ts '''; final playlist = M3u8Parser.parseMediaPlaylist(content, 'https://example.com/event.m3u8'); expect(playlist.isVod, isFalse); }); // ── Explicit VOD tag, no ENDLIST ───────────────────────────────────────── test('Test 6: explicit PLAYLIST-TYPE:VOD no ENDLIST -> isVod=true', () { const content = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, https://example.com/seg_00000.ts #EXTINF:10.0, https://example.com/seg_00001.ts '''; final playlist = M3u8Parser.parseMediaPlaylist(content, 'https://example.com/vod.m3u8'); expect(playlist.isVod, isTrue); const emptyVod = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:10 '''; final emptyPlaylist = M3u8Parser.parseMediaPlaylist(emptyVod, 'https://example.com/empty.m3u8'); expect(emptyPlaylist.isVod, isTrue); expect(emptyPlaylist.segments.length, 0); }); // ── Nested M3U8 expansion ──────────────────────────────────────────────── test('Test 7: nested M3U8 segments are fully resolved', () async { const outerContent = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:30 #EXTINF:10.0, https://example.com/seg_00000.ts #EXTINF:30.0, https://example.com/nested.m3u8 #EXTINF:10.0, https://example.com/seg_00002.ts #EXT-X-ENDLIST '''; const nestedContent = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:10.0, seg_a.ts #EXTINF:10.0, seg_b.ts #EXTINF:10.0, seg_c.ts #EXT-X-ENDLIST '''; final outer = M3u8Parser.parseMediaPlaylist(outerContent, 'https://example.com/main.m3u8'); expect(outer.segments.length, 3); expect(outer.segments.where((s) => s.uri.endsWith('.m3u8')).length, 1); final resolved = await M3u8Parser.resolveNestedSegments( outer.segments, (url) async { if (url.contains('nested.m3u8')) return nestedContent; throw Exception('Unknown URL: $url'); }, ); expect(resolved.length, 5); expect(resolved.any((s) => s.uri.endsWith('.m3u8')), isFalse); expect(resolved[0].uri, 'https://example.com/seg_00000.ts'); expect(resolved[1].uri, 'https://example.com/seg_a.ts'); expect(resolved[2].uri, 'https://example.com/seg_b.ts'); expect(resolved[3].uri, 'https://example.com/seg_c.ts'); expect(resolved[4].uri, 'https://example.com/seg_00002.ts'); }); }); } ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { }); } ================================================ FILE: web/index.html ================================================ kazumi ================================================ FILE: web/manifest.json ================================================ { "name": "kazumi", "short_name": "kazumi", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(kazumi LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "kazumi") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include #include #include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FlutterVolumeControllerPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WebviewWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WebviewWindowsPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color flutter_volume_controller media_kit_libs_windows_video media_kit_video screen_retriever_windows tray_manager url_launcher_windows webview_windows window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "external_player_utils.cpp" "fullscreen_utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Remove the .exp and .lib files generated by the linker. # These files are not needed for the application and can be safely removed. add_custom_command(TARGET ${BINARY_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E remove "$/${BINARY_NAME}.exp" COMMAND ${CMAKE_COMMAND} -E remove "$/${BINARY_NAME}.lib" ) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "kazumi" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "kazumi" "\0" VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "kazumi.exe" "\0" VALUE "ProductName", "kazumi" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/external_player_utils.cpp ================================================ // This file is a part of Kazumi // (https://github.com/Predidit/Kazumi). // // Copyright © 2024 Predidit // All rights reserved. // Use of this source code is governed by GPLv3 license that can be found in the // LICENSE file. #include #include #include #include #include #include #include "external_player_utils.h" void ExternalPlayerUtils::OpenWithPlayer(const char* url) { // temp file path wchar_t tempPath[MAX_PATH]; GetTempPathW(MAX_PATH, tempPath); // Generate a random file name std::wstring randomFileName = L"kazumi_stream_"; std::random_device rd; std::mt19937 eng(rd()); std::uniform_int_distribution<> distr(10000000, 99999999); randomFileName += std::to_wstring(distr(eng)) + L".m3u8"; wchar_t tempFile[MAX_PATH]; wcscpy_s(tempFile, tempPath); wcscat_s(tempFile, randomFileName.c_str()); // write the URL to the temp file std::wofstream outFile(tempFile); if (outFile.is_open()) { outFile << L"#EXTM3U\n"; outFile << std::wstring(url, url + strlen(url)); outFile.close(); } else { return; } SHELLEXECUTEINFO execInfo = {0}; execInfo.cbSize = sizeof(SHELLEXECUTEINFO); execInfo.fMask = SEE_MASK_INVOKEIDLIST; execInfo.lpVerb = L"openas"; execInfo.lpFile = tempFile; execInfo.nShow = SW_SHOWNORMAL; ShellExecuteEx(&execInfo); // DeleteFileW(tempFile); } ================================================ FILE: windows/runner/external_player_utils.h ================================================ // This file is a part of Kazumi // (https://github.com/Predidit/Kazumi). // // Copyright © 2024 Predidit // All rights reserved. // Use of this source code is governed by GPLv3 license that can be found in the // LICENSE file. #ifndef EXTERNAL_PLAYER_UTILS_H_ #define EXTERNAL_PLAYER_UTILS_H_ #include #include class ExternalPlayerUtils { public: static void OpenWithPlayer(const char* url); }; #endif // EXTERNAL_PLAYER_UTILS_H_ ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include "fullscreen_utils.h" #include "external_player_utils.h" #include #include #include #include #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); // Removed automatic window show to let window_manager plugin control visibility // This prevents window flashing during startup // flutter_controller_->engine()->SetNextFrameCallback([&]() { // this->Show(); // }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); // Register Intent MethodChannel RegisterIntentChannel(); // Register Storage MethodChannel RegisterStorageChannel(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } // Intent MethodChannel setup void FlutterWindow::RegisterIntentChannel() { auto window_channel = std::make_unique>( flutter_controller_->engine()->messenger(), "com.predidit.kazumi/intent", &flutter::StandardMethodCodec::GetInstance()); window_channel->SetMethodCallHandler([this](const auto& call, auto result) { if (call.method_name().compare("enterFullscreen") == 0) { FullscreenUtils::EnterNativeFullscreen(GetHandle()); result->Success(); } else if (call.method_name().compare("exitFullscreen") == 0) { FullscreenUtils::ExitNativeFullscreen(GetHandle()); result->Success(); } else if (call.method_name().compare("openWithMime") == 0) { const auto* arguments = std::get_if(call.arguments()); if (arguments) { auto url_it = arguments->find(flutter::EncodableValue("url")); if (url_it != arguments->end()) { const std::string& url = std::get(url_it->second); ExternalPlayerUtils::OpenWithPlayer(url.c_str()); result->Success(); } else { result->Error("InvalidArguments", "Missing 'url' argument"); } } else { result->Error("InvalidArguments", "Arguments are not a map"); } } else { result->NotImplemented(); } }); } // Storage MethodChannel setup void FlutterWindow::RegisterStorageChannel() { auto storage_channel = std::make_unique>( flutter_controller_->engine()->messenger(), "com.predidit.kazumi/storage", &flutter::StandardMethodCodec::GetInstance()); storage_channel->SetMethodCallHandler([](const auto& call, auto result) { if (call.method_name().compare("getAvailableStorage") == 0) { std::wstring path = L"C:\\"; const auto* arguments = std::get_if(call.arguments()); if (arguments) { auto path_it = arguments->find(flutter::EncodableValue("path")); if (path_it != arguments->end()) { const std::string& path_str = std::get(path_it->second); // Extract drive root from path (e.g. "C:\Users\..." -> "C:\") if (path_str.length() >= 2 && path_str[1] == ':') { path = std::wstring(1, static_cast(path_str[0])) + L":\\"; } } } ULARGE_INTEGER free_bytes_available; if (GetDiskFreeSpaceExW(path.c_str(), &free_bytes_available, nullptr, nullptr)) { result->Success(flutter::EncodableValue(static_cast(free_bytes_available.QuadPart))); } else { result->Success(flutter::EncodableValue(static_cast(-1))); } } else { result->NotImplemented(); } }); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; // Register Intent MethodChannel void RegisterIntentChannel(); // Register Storage MethodChannel void RegisterStorageChannel(); }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/fullscreen_utils.cpp ================================================ // This file is a part of media_kit // (https://github.com/media-kit/media-kit). // // Copyright © 2021 & onwards, Hitesh Kumar Saini . // All rights reserved. // Use of this source code is governed by MIT license that can be found in the // LICENSE file. #include "fullscreen_utils.h" void FullscreenUtils::EnterNativeFullscreen(HWND window) { if (fullscreen_) { return; } fullscreen_ = true; // The primary idea here is to revolve around |WS_OVERLAPPEDWINDOW| & // detect/set fullscreen based on it. In the window procedure, this is // separately handled. If there is no |WS_OVERLAPPEDWINDOW| style on the // window i.e. in fullscreen, then no area is left for |WM_NCHITTEST|, // accordingly client area is also expanded to fill whole monitor using // |WM_NCCALCSIZE|. auto style = ::GetWindowLongPtr(window, GWL_STYLE); if (style & WS_OVERLAPPEDWINDOW) { auto monitor = MONITORINFO{}; auto placement = WINDOWPLACEMENT{}; monitor.cbSize = sizeof(MONITORINFO); placement.length = sizeof(WINDOWPLACEMENT); ::GetWindowPlacement(window, &placement); rect_before_fullscreen_ = RECT{ placement.rcNormalPosition.left, placement.rcNormalPosition.top, placement.rcNormalPosition.right, placement.rcNormalPosition.bottom, }; ::GetMonitorInfo(::MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST), &monitor); ::SetWindowLongPtr(window, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW); ::SetWindowPos(window, HWND_TOP, monitor.rcMonitor.left, monitor.rcMonitor.top, monitor.rcMonitor.right - monitor.rcMonitor.left, monitor.rcMonitor.bottom - monitor.rcMonitor.top, SWP_NOOWNERZORDER | SWP_FRAMECHANGED); } } void FullscreenUtils::ExitNativeFullscreen(HWND window) { if (!fullscreen_) { return; } fullscreen_ = false; auto style = ::GetWindowLongPtr(window, GWL_STYLE); if (!(style & WS_OVERLAPPEDWINDOW)) { ::SetWindowLongPtr(window, GWL_STYLE, style | WS_OVERLAPPEDWINDOW); if (::IsZoomed(window)) { // Refresh the parent window. ::SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); auto rect = RECT{}; ::GetClientRect(window, &rect); auto flutter_view = ::FindWindowEx(window, nullptr, kFlutterViewWindowClassName, nullptr); ::SetWindowPos(flutter_view, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); } else { ::SetWindowPos( window, nullptr, rect_before_fullscreen_.left, rect_before_fullscreen_.top, rect_before_fullscreen_.right - rect_before_fullscreen_.left, rect_before_fullscreen_.bottom - rect_before_fullscreen_.top, SWP_NOACTIVATE | SWP_NOZORDER); } } } bool FullscreenUtils::fullscreen_ = false; RECT FullscreenUtils::rect_before_fullscreen_ = RECT{}; ================================================ FILE: windows/runner/fullscreen_utils.h ================================================ // This file is a part of media_kit // (https://github.com/media-kit/media-kit). // // Copyright © 2021 & onwards, Hitesh Kumar Saini . // All rights reserved. // Use of this source code is governed by MIT license that can be found in the // LICENSE file. #ifndef FULLSCREEN_UTILS_H_ #define FULLSCREEN_UTILS_H_ #include #include class FullscreenUtils { public: static void EnterNativeFullscreen(HWND window); static void ExitNativeFullscreen(HWND window); private: static constexpr auto kFlutterViewWindowClassName = L"FLUTTERVIEW"; static bool fullscreen_; static RECT rect_before_fullscreen_; }; #endif // FULLSCREEN_UTILS_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" // recommended by NVIDIA to enable high-performance GPU extern "C" { __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; } // recommended by AMD to enable high-performance GPU extern "C" { __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } HANDLE mutex = NULL; // Window class name must match the one in win32_window.cpp constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; bool ActivateExistingWindow() { // Find the existing window by class name HWND hwnd = ::FindWindow(kWindowClassName, L"kazumi"); if (hwnd != NULL) { // Check if window is hidden (e.g., minimized to tray) if (!::IsWindowVisible(hwnd)) { // Show the hidden window ::ShowWindow(hwnd, SW_SHOW); } // If window is minimized, restore it else if (::IsIconic(hwnd)) { ::ShowWindow(hwnd, SW_RESTORE); } // Bring window to foreground ::SetForegroundWindow(hwnd); // Flash the window to get user's attention FLASHWINFO fwi = {0}; fwi.cbSize = sizeof(FLASHWINFO); fwi.hwnd = hwnd; fwi.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG; fwi.uCount = 3; fwi.dwTimeout = 0; ::FlashWindowEx(&fwi); return true; } return false; } bool isSingleInstance() { if (mutex != NULL) { return true; } std::wstring mutex_str = L"kazumi.win.mutex"; mutex = ::CreateMutex(NULL, TRUE, mutex_str.c_str()); if (mutex == NULL || GetLastError() == ERROR_ALREADY_EXISTS) { CloseHandle(mutex); mutex = NULL; return false; } return true; } int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Make sure the application is a single instance. // This is important for the application to work correctly with the local storage. if (!isSingleInstance()) { // Try to activate the existing window instead of showing an error ActivateExistingWindow(); return EXIT_SUCCESS; } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); // Disable thread merge to improve performance // Attention: This may impact plugin performance and may be incompatible with future Flutter releases. project.set_ui_thread_policy(flutter::UIThreadPolicy::RunOnSeparateThread); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"kazumi", origin, size)) { if (mutex) { CloseHandle(mutex); } return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); if (mutex) { CloseHandle(mutex); } return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length <= 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (LOWORD(wparam) == WA_ACTIVE || LOWORD(wparam) == WA_CLICKACTIVE) { if (child_content_ != nullptr) { SetFocus(child_content_); } } return 0; case WM_SETFOCUS: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_