├── .github ├── dlls │ ├── README.md │ ├── media_kit_libs_windows_video_plugin.dll │ ├── media_kit_native_event_loop.dll │ └── media_kit_video_plugin.dll ├── images │ ├── Bangumi-search.png │ ├── hooray-found.png │ ├── oops-not-found.png │ └── source-selection.png └── workflows │ ├── auto-prerelease.yaml │ └── auto-release.yaml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── knkpanime │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ ├── NotoSerifHK-Bold.ttf │ ├── NotoSerifHK-Regular.ttf │ └── NotoSerifHK-VariableFont_wght.ttf └── images │ ├── icon.jpg │ ├── no_image.jpg │ ├── placeholder.jpg │ └── sidemenu_background.jpg ├── devtools_options.yaml ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ └── short_description.txt │ └── zh-CN │ ├── full_description.txt │ └── short_description.txt ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── 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 ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── adapters │ ├── adapter_base.dart │ ├── adapter_registry.dart │ ├── anime1_adapter.dart │ ├── ant_adapter.dart │ ├── bimi_adapter.dart │ ├── girigirilove_adapter.dart │ ├── iyf_adapter.dart │ ├── js_adapter.dart │ ├── models │ │ ├── anime1_anime_info.dart │ │ └── yhmd_response.dart │ ├── nyafun_adapter.dart │ └── yhdm_adapter.dart ├── app_module.dart ├── app_widget.dart ├── main.dart ├── models │ ├── anime_info.dart │ ├── anime_info.g.dart │ ├── danmaku.dart │ ├── episode.dart │ ├── episode.g.dart │ ├── history.dart │ ├── history.g.dart │ ├── image_set.dart │ ├── image_set.g.dart │ ├── series.dart │ ├── series.g.dart │ └── source.dart ├── navigation.dart ├── pages │ ├── calendar │ │ ├── calendar_controller.dart │ │ ├── calendar_controller.g.dart │ │ ├── calendar_module.dart │ │ └── calendar_page.dart │ ├── favorite │ │ ├── favorite_controller.dart │ │ ├── favorite_module.dart │ │ └── favorite_page.dart │ ├── history │ │ ├── history_controller.dart │ │ ├── history_module.dart │ │ └── history_page.dart │ ├── play │ │ ├── play_module.dart │ │ ├── play_page.dart │ │ ├── player_controller.dart │ │ └── player_controller.g.dart │ ├── search │ │ ├── adapter_search_controller.dart │ │ ├── adapter_search_controller.g.dart │ │ ├── adapter_search_page.dart │ │ ├── bangumi_search_controller.dart │ │ ├── bangumi_search_controller.g.dart │ │ ├── bangumi_search_page.dart │ │ └── search_module.dart │ └── settings │ │ ├── image_set_config_page.dart │ │ ├── js_adapter_config_page.dart │ │ ├── settings_controller.dart │ │ ├── settings_module.dart │ │ └── settings_page.dart ├── utils │ ├── bangumi.dart │ ├── danmaku.dart │ ├── storage.dart │ ├── utils.dart │ └── webview.dart └── widgets │ ├── anime_card.dart │ ├── danmaku_settings_window.dart │ ├── desktop_player.dart │ ├── mobile_player.dart │ ├── series_card.dart │ └── source_selection_window.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 ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── create_shortcut.bat ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/dlls/README.md: -------------------------------------------------------------------------------- 1 | This folder stores the dlls for substitution after github workflow compilation. 2 | 3 | Reference: https://github.com/KNKPA/KNKPAnime/issues/21 -------------------------------------------------------------------------------- /.github/dlls/media_kit_libs_windows_video_plugin.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/dlls/media_kit_libs_windows_video_plugin.dll -------------------------------------------------------------------------------- /.github/dlls/media_kit_native_event_loop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/dlls/media_kit_native_event_loop.dll -------------------------------------------------------------------------------- /.github/dlls/media_kit_video_plugin.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/dlls/media_kit_video_plugin.dll -------------------------------------------------------------------------------- /.github/images/Bangumi-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/images/Bangumi-search.png -------------------------------------------------------------------------------- /.github/images/hooray-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/images/hooray-found.png -------------------------------------------------------------------------------- /.github/images/oops-not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/images/oops-not-found.png -------------------------------------------------------------------------------- /.github/images/source-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/.github/images/source-selection.png -------------------------------------------------------------------------------- /.github/workflows/auto-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "release" 3 | 4 | on: 5 | push: 6 | tags: 7 | - "1.*" 8 | workflow_dispatch: 9 | inputs: 10 | logLevel: 11 | description: "Log level" 12 | required: true 13 | default: "warning" 14 | 15 | env: 16 | APP_ID: ${{ secrets.APP_ID }} 17 | APP_SECRET: ${{ secrets.APP_SECRET }} 18 | 19 | jobs: 20 | flutter-build: 21 | name: "Release" 22 | runs-on: "windows-latest" 23 | permissions: write-all 24 | 25 | steps: 26 | - name: Clone repository 27 | uses: actions/checkout@v4 28 | - run: git config --system core.longpaths true # to avoid file name too long error 29 | - run: choco install yq 30 | - name: Set up Flutter 31 | uses: subosito/flutter-action@v2 32 | with: 33 | channel: stable 34 | flutter-version-file: pubspec.yaml 35 | cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache 36 | cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path 37 | pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies 38 | pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path 39 | - name: Set up Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: "temurin" 43 | java-version: "18" 44 | - run: flutter pub get 45 | - run: flutter build apk --dart-define=APP_ID=${{ secrets.APP_ID }} --dart-define=APP_SECRET=${{ secrets.APP_SECRET }} 46 | - run: flutter build windows --dart-define=APP_ID=${{ secrets.APP_ID }} --dart-define=APP_SECRET=${{ secrets.APP_SECRET }} 47 | 48 | # Substitute DLLs 49 | - run: xcopy ".github\dlls\*.dll" "build\windows\x64\runner\Release\" /Y 50 | 51 | - run: Compress-Archive build/windows/x64/runner/Release knkpanime-windows.zip 52 | - uses: r0adkll/sign-android-release@v1 53 | name: Sign app APK 54 | id: sign_app 55 | with: 56 | releaseDirectory: build/app/outputs/apk/release 57 | signingKeyBase64: ${{ secrets.SIGNING_KEY }} 58 | alias: ${{ secrets.ALIAS }} 59 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} 60 | env: 61 | BUILD_TOOLS_VERSION: "34.0.0" 62 | - run: Ren ${{ steps.sign_app.outputs.signedReleaseFile }} knkpanime-android.apk 63 | - uses: "marvinpinto/action-automatic-releases@latest" 64 | with: 65 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 66 | prerelease: false 67 | files: | 68 | knkpanime-windows.zip 69 | build/app/outputs/apk/release/knkpanime-android.apk 70 | - name: Upload outputs 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: outputs 74 | path: | 75 | knkpanime-windows.zip 76 | build/app/outputs/apk/release/knkpanime-android.apk 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | .vscode/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "300451adae589accbece3490f4396f10bdf15e6e" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 17 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 18 | - platform: android 19 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 20 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 21 | - platform: ios 22 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 23 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 24 | - platform: linux 25 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 26 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 27 | - platform: macos 28 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 29 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 30 | - platform: web 31 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 32 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 33 | - platform: windows 34 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 35 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # knkpanime 2 | 3 | 一个支持多番剧源和弹幕的看番(甚至看电视剧)软件,甚至可以自定义你想接入的网站适配器! 4 | 5 | 有问题或建议欢迎通过issue反馈。 6 | 7 | ## 下载 8 | 9 | [Get it on F-Droid](https://f-droid.org/packages/com.example.knkpanime) 12 | 13 | [下载链接](https://github.com/KNKPA/KNKPAnime/releases/latest) 14 | 15 | 另:macOS版可以在[预览版](https://github.com/KNKPA/KNKPAnime/releases/tag/latest)中下载,但不知为什么,github workflow编译的macOS版在我的mac上打开视频播放页时会崩溃(目前猜测是libmpv问题),而本地编译的则不会。如果mac用户想用但在github的release中下载的软件打不开的话,可以尝试自行编译: 16 | 17 | ``` 18 | [[ $(uname -m) == 'x86_64' ]] && wget https://storage.flutter-io.cn/flutter_infra_release/releases/stable/macos/flutter_macos_3.19.5-stable.zip -O flutter.zip || wget https://storage.flutter-io.cn/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.19.5-stable.zip -O flutter.zip 19 | unzip flutter.zip 20 | git clone https://github.com/KNKPA/KNKPAnime.git # 如果访问github有问题可以选择国内镜像 21 | cd KNKPAnime 22 | ../flutter/bin/flutter build macos --release 23 | mv build/macos/Build/Products/Release/knkpanime.app ../ 24 | cd .. 25 | rm -rf KNKPAnime flutter flutter.zip 26 | ``` 27 | 28 | ## 介绍 29 | 30 | 最重要的当然就是搜索啦。作为一个支持多番剧源的软件,你可以先在Bangumi上搜索想看的番剧,再选择可用的源观看: 31 | 32 | ![Bangumi search](.github/images/Bangumi-search.png) 33 | 34 | ![source selection](.github/images/source-selection.png) 35 | 36 | 当然,有的时候也会因为不同译名或番剧名中有特殊符号而导致使用Bangumi搜索很难匹配到番剧源中的数据,这个时候就可以用番剧源搜索来自定义用来搜索的词: 37 | 38 | ![oops, not found](.github/images/oops-not-found.png) 39 | 40 | ![hooray! found](.github/images/hooray-found.png) 41 | 42 | 需要注意的是,在”Bangumi搜索“页面搜索时,会使用Bangumi提供的番剧名以及你所输入的搜索词在各番剧源进行两次搜索;而在追番、新番日历这两个页面点击某一个番剧的时候,只会用Bangumi提供的番剧名进行一次搜索,因此可能会出现无法搜索到的情况。如果这种情况发生的话,请在两个搜索界面搜索或通过历史记录进入(如果你看过这部番的话)。 43 | 44 | 然后就是一些比较常规的功能 - 历史记录、追番、番剧更新日历等,应该不需要过多介绍。 45 | 46 | ## 桌面版快捷键 47 | 48 | | 快捷键 | 对应操作 | 49 | |-------|-------| 50 | | J | 快进90秒(跳过OP/ED)| 51 | | 左右箭头 | 快进/快退10秒 | 52 | | 上下箭头 | 音量增加/减少5% | 53 | | esc | 退出全屏 | 54 | | D | 开启/关闭弹幕 | 55 | | F | 开启/退出全屏 | 56 | | [ | 上一集 | 57 | | ] | 下一集 | 58 | 59 | ## 自定义适配器 60 | 61 | 所谓适配器,就是解析在线观看网站的接口从而获取搜索结果、视频资源并提供给播放器播放的代码模块。在船新的1.1.0版本中,本软件已经可以支持两种适配器的定义形式以及解析形式,分别为: 62 | 63 | ### 定义形式 64 | 65 | #### 内置适配器 66 | 67 | 这种适配器使用dart代码编写,直接随主程序编译,运行速度和资源消耗上最有优势。当然,既然是随主程序一起编译,代价就是 68 | - 加入新的适配器必须通过向主程序提交代码的方式,即PR,并需要我的允许。~~当然我也不会不允许就是了(~~ 69 | - 新的适配器以及修复过期链接等操作必须随新版本发布,并不灵活。 70 | 71 | #### JavaScript适配器 72 | 73 | 通过使用[flutter_js](https://github.com/abner/flutter_js),软件内置了一个JavaScript runtime,可以用来即时执行JS代码。利用这个JS runtime,我们可以随时获取互联网上的适配器并添加到软件中。虽然损失了一些性能,但毕竟搜索和解析视频都是相对不频繁的操作(相对于UI等等耗能大户来说),因此不会造成太大影响。 74 | 75 | 关于如何定义自己的适配器,请参见[KNKPAnime-js-adapters](https://github.com/KNKPA/KNKPAnime-js-adapters)。 76 | 77 | **请从您信任的来源添加适配器** 78 | 79 | ### 解析形式 80 | 81 | #### 基于规则 82 | 83 | 基于手写规则的解析,需要编写者去检查网站代码并找出视频源的URL到底如何得到。 84 | 85 | #### 基于WebView 86 | 87 | 通过在软件中跑一个不可见的浏览器,我们可以用他山之石来攻他山的玉,通过执行原网站的全部操作并获取视频链接。 88 | 89 | ## Acknowledgement 90 | 91 | 本项目受[oneAnime](https://github.com/Predidit/oneAnime)启发,并在Anime1的适配器中借用了其代码。 92 | 93 | 本项目使用了[Bangumi](http://bangumi.tv/)、[dandanplay](https://www.dandanplay.com/)的开放API。网站运营不易,请各位在能力范围内尽量支持这两个网站的运营。 94 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | prefer_function_declarations_over_variables: false 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.knkpanime" 27 | compileSdk flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.knkpanime" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 21 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | //signingConfig signingConfigs.debug 59 | ndk { 60 | // The line below only builds arm64-v8a 61 | abiFilters 'armeabi-v7a','arm64-v8a','x86_64' // this includes all build targets 62 | } 63 | } 64 | } 65 | } 66 | 67 | flutter { 68 | source '../..' 69 | } 70 | 71 | dependencies {} 72 | 73 | ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3] 74 | import com.android.build.OutputFile 75 | android.applicationVariants.all { variant -> 76 | variant.outputs.each { output -> 77 | def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) 78 | if (abiVersionCode != null) { 79 | output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 24 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/knkpanime/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.knkpanime 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "7.3.0" apply false 23 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /assets/fonts/NotoSerifHK-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/fonts/NotoSerifHK-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/NotoSerifHK-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/fonts/NotoSerifHK-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/NotoSerifHK-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/fonts/NotoSerifHK-VariableFont_wght.ttf -------------------------------------------------------------------------------- /assets/images/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/images/icon.jpg -------------------------------------------------------------------------------- /assets/images/no_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/images/no_image.jpg -------------------------------------------------------------------------------- /assets/images/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/images/placeholder.jpg -------------------------------------------------------------------------------- /assets/images/sidemenu_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/assets/images/sidemenu_background.jpg -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | A anime app which supports multiple anime source and danmaku. You can even add custom website adaptor! 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Anime app with multiple source and danmaku support 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | 一个支持多番剧源和弹幕的看番(甚至看电视剧)软件,甚至可以自定义你想接入的网站适配器! 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 支持多番剧源与弹幕的看番软件 2 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Knkpanime 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | knkpanime 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/adapters/adapter_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:knkpanime/models/series.dart'; 2 | import 'package:knkpanime/models/episode.dart'; 3 | import 'package:knkpanime/models/source.dart'; 4 | import 'package:media_kit_video/media_kit_video.dart'; 5 | 6 | enum SearchStatus { pending, success, failed } 7 | 8 | /// The base class that all the adapters must inherit. 9 | abstract class AdapterBase { 10 | /// Source name, e.g.: Anime1, yhdm 11 | String name; 12 | 13 | /// Source description, will be displayed on the source selection window if available. 14 | String? description; 15 | 16 | /// Status to reflect each search. 17 | SearchStatus status = SearchStatus.success; 18 | 19 | /// Wether this adapter uses webview to get video link or other resources 20 | bool useWebview; 21 | 22 | AdapterBase(this.name, {this.description, this.useWebview = false}); 23 | 24 | /// Given an anime name, search possible resources. 25 | /// [bangumiName] is the name provided by bgm.tv, which could be 26 | /// very detailed but hard to match. 27 | /// [searchKeyword] is the user input, usually simple and short but 28 | /// might not be precise. 29 | /// 30 | /// It is at the subclasses' will to decide which to use and how to use them, 31 | /// and it's also the subclasses' responsibility to update the status. 32 | Future> search(String bangumiName, String searchKeyword); 33 | 34 | /// Given an seriesId returned by [search], get video sources of that anime. 35 | Future> getSources(String seriesId); 36 | 37 | /// Pass the control of player to the adapter to initialize the player. 38 | /// This function should handle all the details such as authentication. 39 | Future play(String episodeId, VideoController controller); 40 | 41 | @override 42 | String toString() { 43 | return name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/adapters/adapter_registry.dart: -------------------------------------------------------------------------------- 1 | import 'package:knkpanime/adapters/adapter_base.dart'; 2 | import 'package:knkpanime/adapters/anime1_adapter.dart'; 3 | import 'package:knkpanime/adapters/bimi_adapter.dart'; 4 | import 'package:knkpanime/adapters/girigirilove_adapter.dart'; 5 | import 'package:knkpanime/adapters/iyf_adapter.dart'; 6 | import 'package:knkpanime/adapters/ant_adapter.dart'; 7 | import 'package:knkpanime/adapters/nyafun_adapter.dart'; 8 | import 'package:knkpanime/adapters/yhdm_adapter.dart'; 9 | 10 | final adapters = [ 11 | GirigiriLoveAdapter(), 12 | BimiAdapter(), 13 | //YhdmAdapter(), 14 | Anime1Adapter(), 15 | IyfAdapter(), 16 | AntAdapter(), 17 | NyafunAdapter(), 18 | ]; 19 | -------------------------------------------------------------------------------- /lib/adapters/girigirilove_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_modular/flutter_modular.dart'; 5 | import 'package:html/parser.dart'; 6 | import 'package:knkpanime/adapters/adapter_base.dart'; 7 | import 'package:knkpanime/models/episode.dart'; 8 | import 'package:knkpanime/models/series.dart'; 9 | import 'package:knkpanime/models/source.dart'; 10 | import 'package:logger/logger.dart'; 11 | import 'package:media_kit/media_kit.dart'; 12 | import 'package:media_kit_video/media_kit_video.dart'; 13 | 14 | class GirigiriLoveAdapter extends AdapterBase { 15 | final dio = Dio(BaseOptions(headers: { 16 | 'User-Agent': 17 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.3', 18 | })); 19 | 20 | final String baseUrl = 'https://anime.girigirilove.com'; 21 | final String searchApi = 22 | 'https://anime.girigirilove.com/search/-------------/?wd='; 23 | final String seriesApi = 'https://anime.girigirilove.com/'; 24 | final String playApi = 'https://anime.girigirilove.com/'; 25 | late final logger = Modular.get(); 26 | 27 | @override 28 | Future> getSources(String seriesId) async { 29 | var resp = await dio.get(seriesApi + seriesId); 30 | return _parseSeries(resp.data.toString()); 31 | } 32 | 33 | @override 34 | Future play(String episodeId, VideoController controller) async { 35 | var resp = await dio.get(playApi + episodeId); 36 | await controller.player.open(Media(_parsePlayLink(resp.data.toString()))); 37 | } 38 | 39 | @override 40 | Future> search(String bangumiName, String searchKeyword) async { 41 | List ret = []; 42 | status = SearchStatus.pending; 43 | try { 44 | if (bangumiName.isNotEmpty) { 45 | var resp = await dio.get(searchApi + bangumiName); 46 | ret.addAll(_parseSearchResult(resp.data.toString())); 47 | } 48 | if (searchKeyword.isNotEmpty) { 49 | var resp = await dio.get(searchApi + searchKeyword); 50 | ret.addAll(_parseSearchResult(resp.data.toString())); 51 | } 52 | status = SearchStatus.success; 53 | } catch (e) { 54 | status = SearchStatus.failed; 55 | logger.w(e); 56 | rethrow; 57 | } 58 | logger.i('Girigiri love adapter returns ${ret.length} results'); 59 | return ret; 60 | } 61 | 62 | List _parseSearchResult(String html) { 63 | var doc = parse(html); 64 | return doc.getElementsByClassName('public-list-box').map((div) { 65 | String? image; 66 | var style = div.querySelector('.cover')?.attributes['style']; 67 | var regExp = RegExp(r'url\((.*?)\)'); 68 | if (style != null && regExp.firstMatch(style) != null) { 69 | var url = regExp.firstMatch(style)!.group(1); 70 | image = baseUrl + url!; 71 | } 72 | String name = div.querySelector('.thumb-txt')!.text; 73 | String desc = div.querySelector('.thumb-blurb')!.text; 74 | String id = div 75 | .querySelector('.thumb-menu')! 76 | .firstChild! 77 | .attributes['href']! 78 | .replaceAll('/', ''); 79 | return Series(id, name, description: desc, image: image); 80 | }).toList(); 81 | } 82 | 83 | List _parseSeries(String html) { 84 | var doc = parse(html); 85 | var uls = doc.querySelectorAll('.anthology-list-play')!; 86 | List ret = []; 87 | uls.forEach((ul) { 88 | List episodes = []; 89 | ul.querySelectorAll('li').asMap().forEach((idx, li) { 90 | episodes.add(Episode( 91 | li.firstChild!.attributes['href']!.replaceAll('/', ''), idx)); 92 | }); 93 | ret.add(Source(episodes)); 94 | }); 95 | return ret; 96 | } 97 | 98 | String _parsePlayLink(String html) { 99 | var doc = parse(html); 100 | var div = doc.querySelector('.player-left'); 101 | div ??= doc.querySelector('.player-top'); 102 | var script = 103 | div!.querySelector('script')!.text.split(',').map((e) => e.trim()); 104 | for (var line in script) { 105 | if (line.contains("\"url\"")) { 106 | var encoded = 107 | line.split(':')[1].replaceAll("\"", '').replaceAll(',', ''); 108 | var decoded = base64Decode(encoded); 109 | var urlEncoded = String.fromCharCodes(decoded); 110 | var videoLink = Uri.decodeFull(urlEncoded); 111 | logger.i('Parsed video link: $videoLink'); 112 | return videoLink; 113 | } 114 | } 115 | return ''; 116 | } 117 | 118 | GirigiriLoveAdapter() : super('Girigiri Love'); 119 | } 120 | -------------------------------------------------------------------------------- /lib/adapters/js_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 7 | import 'package:flutter_js/extensions/fetch.dart'; 8 | import 'package:flutter_modular/flutter_modular.dart'; 9 | import 'package:knkpanime/adapters/adapter_base.dart'; 10 | import 'package:flutter_js/flutter_js.dart'; 11 | import 'package:knkpanime/models/series.dart'; 12 | import 'package:knkpanime/models/source.dart'; 13 | import 'package:knkpanime/utils/webview.dart'; 14 | import 'package:logger/logger.dart'; 15 | import 'package:media_kit/media_kit.dart'; 16 | import 'package:media_kit_video/media_kit_video.dart'; 17 | 18 | class JSAdapter extends AdapterBase { 19 | late final jsEngine = getJavascriptRuntime(); 20 | late final String jsSource; 21 | final completer = Completer(); 22 | bool initialized = false; 23 | late final logger = Modular.get(); 24 | final String sourceUrl; 25 | 26 | @override 27 | Future> getSources(String seriesId) async { 28 | await initializedOrThrow(); 29 | final promise = 30 | jsEngine.evaluate("$jsSource\nadapter.getSources('$seriesId');"); 31 | jsEngine.executePendingJob(); 32 | final result = await jsEngine.handlePromise(promise); 33 | final ret = (jsonDecode(result.stringResult) as List) 34 | .map((sourceJson) => Source.fromDynamicJson(sourceJson)) 35 | .toList(); 36 | return ret; 37 | } 38 | 39 | @override 40 | Future play(String episodeId, VideoController controller) async { 41 | // For webview adapters, the js function `getVideoResource` should return 42 | // the url of the play page. 43 | // Otherwise, it should return the media resource's url. 44 | await initializedOrThrow(); 45 | final promise = 46 | jsEngine.evaluate("$jsSource\nadapter.getVideoResource('$episodeId');"); 47 | jsEngine.executePendingJob(); 48 | final result = await jsEngine.handlePromise(promise); 49 | if (useWebview) { 50 | controller.player.open( 51 | Media((await Webview.getVideoResourceUrl(result.stringResult))!)); 52 | } else { 53 | controller.player.open(Media(result.stringResult)); 54 | } 55 | } 56 | 57 | @override 58 | Future> search(String bangumiName, String searchKeyword) async { 59 | await initializedOrThrow(); 60 | status = SearchStatus.pending; 61 | List ret = []; 62 | try { 63 | if (bangumiName.isNotEmpty) { 64 | final promise = 65 | jsEngine.evaluate("$jsSource\nadapter.search('$bangumiName');"); 66 | jsEngine.executePendingJob(); 67 | final result = await jsEngine.handlePromise(promise); 68 | ret.addAll((jsonDecode(result.stringResult) as List) 69 | .map((json) => Series.fromDynamicJson(json))); 70 | } 71 | if (searchKeyword.isNotEmpty) { 72 | final promise = 73 | jsEngine.evaluate("$jsSource\nadapter.search('$searchKeyword');"); 74 | jsEngine.executePendingJob(); 75 | final result = await jsEngine.handlePromise(promise); 76 | ret.addAll((jsonDecode(result.stringResult) as List) 77 | .map((json) => Series.fromDynamicJson(json))); 78 | } 79 | } catch (e) { 80 | logger.w(e); 81 | rethrow; 82 | } finally { 83 | status = SearchStatus.failed; 84 | } 85 | status = SearchStatus.success; 86 | logger.i('$name adapter returns ${ret.length} results'); 87 | 88 | return ret; 89 | } 90 | 91 | JSAdapter(this.sourceUrl) : super('') { 92 | init(); 93 | } 94 | 95 | void init() async { 96 | try { 97 | await Dio() 98 | .get(sourceUrl) 99 | .then((resp) => jsSource = resp.data.toString()); 100 | jsEngine.enableHandlePromises(); 101 | await jsEngine.enableFetch(); 102 | final configJson = jsEngine.evaluate("$jsSource\nadapter.getConfig();"); 103 | final config = jsonDecode(configJson.stringResult); 104 | name = config['name']!; 105 | description = config['description']; 106 | useWebview = config['useWebview'] ?? false; 107 | completer.complete(true); 108 | initialized = true; 109 | } catch (e) { 110 | logger.w(e); 111 | completer.complete(false); 112 | } 113 | } 114 | 115 | Future initializedOrThrow() async { 116 | await completer.future; 117 | if (!initialized) throw ('该视频源初始化失败'); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/adapters/models/anime1_anime_info.dart: -------------------------------------------------------------------------------- 1 | part of '../anime1_adapter.dart'; 2 | 3 | class Anime1AnimeInfo { 4 | int id; 5 | String name; 6 | 7 | Anime1AnimeInfo(this.id, this.name); 8 | } 9 | -------------------------------------------------------------------------------- /lib/adapters/models/yhmd_response.dart: -------------------------------------------------------------------------------- 1 | class YhdmResponse { 2 | int aid; 3 | int playindex; 4 | int epindex; 5 | 6 | YhdmResponse(this.aid, this.playindex, this.epindex); 7 | 8 | Map toJson() { 9 | return { 10 | 'aid': aid, 11 | 'playindex': playindex, 12 | 'epindex': epindex, 13 | }; 14 | } 15 | 16 | static YhdmResponse extractNumbers(String input) { 17 | RegExp regex = RegExp(r'/(\d+)-(\d+)-(\d+)\.html'); 18 | RegExpMatch? match = regex.firstMatch(input); 19 | if (match != null) { 20 | int aid = int.parse(match.group(1)!); 21 | int playindex = int.parse(match.group(2)!); 22 | int epindex = int.parse(match.group(3)!); 23 | return YhdmResponse(aid, playindex, epindex); 24 | } 25 | throw Exception('No match found'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/adapters/nyafun_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_modular/flutter_modular.dart'; 5 | import 'package:html/parser.dart'; 6 | import 'package:knkpanime/adapters/adapter_base.dart'; 7 | import 'package:knkpanime/models/episode.dart'; 8 | import 'package:knkpanime/models/series.dart'; 9 | import 'package:knkpanime/models/source.dart'; 10 | import 'package:knkpanime/utils/webview.dart'; 11 | import 'package:logger/logger.dart'; 12 | import 'package:media_kit/media_kit.dart'; 13 | import 'package:media_kit_video/media_kit_video.dart'; 14 | 15 | class NyafunAdapter extends AdapterBase { 16 | final dio = Dio(BaseOptions(headers: { 17 | 'User-Agent': 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.3', 19 | })); 20 | 21 | final String baseUrl = 'https://www.nyafun.net'; 22 | final String searchApi = 'https://www.nyafun.net/search.html?wd='; 23 | final String seriesApi = 'https://www.nyafun.net'; 24 | final String playApi = 'https://www.nyafun.net'; 25 | late final logger = Modular.get(); 26 | 27 | @override 28 | Future> getSources(String seriesId) async { 29 | var resp = await dio.get(seriesApi + seriesId); 30 | return _parseSeries(resp.data.toString()); 31 | } 32 | 33 | @override 34 | Future play(String episodeId, VideoController controller) async { 35 | String url = (await Webview.getVideoResourceUrl(playApi + episodeId))!; 36 | await controller.player.open(Media(url, httpHeaders: {'Referer': url})); 37 | } 38 | 39 | @override 40 | Future> search(String bangumiName, String searchKeyword) async { 41 | List ret = []; 42 | status = SearchStatus.pending; 43 | try { 44 | if (bangumiName.isNotEmpty) { 45 | var resp = await dio.get(searchApi + bangumiName); 46 | ret.addAll(_parseSearchResult(resp.data.toString())); 47 | } 48 | if (searchKeyword.isNotEmpty) { 49 | var resp = await dio.get(searchApi + searchKeyword); 50 | ret.addAll(_parseSearchResult(resp.data.toString())); 51 | } 52 | status = SearchStatus.success; 53 | } catch (e) { 54 | status = SearchStatus.failed; 55 | logger.w(e); 56 | rethrow; 57 | } 58 | logger.i('Girigiri love adapter returns ${ret.length} results'); 59 | return ret; 60 | } 61 | 62 | List _parseSearchResult(String html) { 63 | var doc = parse(html); 64 | return doc.getElementsByClassName('public-list-box').map((div) { 65 | final a = div.querySelector('.thumb-txt.cor4.hide')!.querySelector('a')!; 66 | String? image = div.querySelector('img')?.attributes['data-src']; 67 | String name = a.text; 68 | String? desc = div.querySelector('.cor5.thumb-blurb.hide2')?.text; 69 | String id = a.attributes['href']!; 70 | return Series(id, name, description: desc, image: image); 71 | }).toList(); 72 | } 73 | 74 | List _parseSeries(String html) { 75 | var doc = parse(html); 76 | var uls = doc.querySelectorAll('.anthology-list-play'); 77 | List ret = []; 78 | uls.forEach((ul) { 79 | List episodes = []; 80 | ul.querySelectorAll('li').asMap().forEach((idx, li) { 81 | final a = li.querySelector('a')!; 82 | episodes.add(Episode(a.attributes['href']!, idx, a.text)); 83 | }); 84 | ret.add(Source(episodes)); 85 | }); 86 | return ret; 87 | } 88 | 89 | String _parsePlayLink(String html) { 90 | var doc = parse(html); 91 | var div = doc.querySelector('.player-left'); 92 | div ??= doc.querySelector('.player-top'); 93 | var scriptText = div! 94 | .querySelectorAll('script') 95 | .fold('', (previousValue, element) => previousValue + element.text) 96 | .split(',') 97 | .map((e) => e.trim()); 98 | for (var line in scriptText) { 99 | if (line.contains("\"url\"")) { 100 | var encoded = 101 | line.split(':')[1].replaceAll("\"", '').replaceAll(',', ''); 102 | var decoded = base64Decode(encoded); 103 | var urlEncoded = String.fromCharCodes(decoded); 104 | var videoLink = Uri.decodeFull(urlEncoded); 105 | logger.i('Parsed video link: $videoLink'); 106 | return videoLink; 107 | } 108 | } 109 | return ''; 110 | } 111 | 112 | NyafunAdapter() : super('Nyafun', useWebview: true); 113 | } 114 | -------------------------------------------------------------------------------- /lib/app_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide SearchController; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/pages/calendar/calendar_module.dart'; 4 | import 'package:knkpanime/pages/favorite/favorite_controller.dart'; 5 | import 'package:knkpanime/pages/favorite/favorite_module.dart'; 6 | import 'package:knkpanime/pages/history/history_controller.dart'; 7 | import 'package:knkpanime/pages/history/history_module.dart'; 8 | import 'package:knkpanime/pages/play/play_module.dart'; 9 | import 'package:knkpanime/pages/search/adapter_search_controller.dart'; 10 | import 'package:knkpanime/pages/search/search_module.dart'; 11 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 12 | import 'package:knkpanime/pages/settings/settings_module.dart'; 13 | import 'package:knkpanime/pages/calendar/calendar_controller.dart'; 14 | import 'package:knkpanime/pages/search/bangumi_search_controller.dart'; 15 | import 'package:logger/logger.dart'; 16 | 17 | class AppModule extends Module { 18 | @override 19 | void binds(i) { 20 | i.addInstance(Logger()); 21 | i.addSingleton(SettingsController.new); 22 | i.addSingleton(CalendarController.new); 23 | i.addSingleton(BangumiSearchController.new); 24 | i.addSingleton(HistoryController.new); 25 | i.addSingleton(FavoriteController.new); 26 | i.addSingleton(AdapterSearchController.new); 27 | } 28 | 29 | @override 30 | void routes(r) { 31 | r.module('/search/', 32 | module: SearchModule(), transition: TransitionType.noTransition); 33 | r.module('/play/', 34 | module: PlayModule(), transition: TransitionType.noTransition); 35 | r.module('/history/', 36 | module: HistoryModule(), transition: TransitionType.noTransition); 37 | r.module('/favorite/', 38 | module: FavoriteModule(), transition: TransitionType.noTransition); 39 | r.module('/calendar/', 40 | module: CalendarModule(), transition: TransitionType.noTransition); 41 | r.module('/settings/', 42 | module: SettingsModule(), transition: TransitionType.noTransition); 43 | } 44 | } 45 | 46 | const routes = [ 47 | { 48 | 'path': '/search/bangumi', 49 | 'name': 'Bangumi搜索', 50 | 'icon': Icon(Icons.search), 51 | 'bottom': false, 52 | }, 53 | { 54 | 'path': '/search/adapter', 55 | 'name': '番剧源搜索', 56 | 'icon': Icon(Icons.search), 57 | 'bottom': false, 58 | }, 59 | { 60 | 'path': '/history/', 61 | 'name': '历史记录', 62 | 'icon': Icon(Icons.history), 63 | 'bottom': false, 64 | }, 65 | { 66 | 'path': '/favorite/', 67 | 'name': '追番', 68 | 'icon': Icon(Icons.favorite), 69 | 'bottom': false, 70 | }, 71 | { 72 | 'path': '/calendar/', 73 | 'name': '新番日历', 74 | 'icon': Icon(Icons.calendar_month), 75 | 'bottom': false, 76 | }, 77 | { 78 | 'path': '/settings/', 79 | 'name': '设置', 80 | 'icon': Icon(Icons.settings), 81 | 'bottom': true, 82 | }, 83 | ]; 84 | -------------------------------------------------------------------------------- /lib/app_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:adaptive_theme/adaptive_theme.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_modular/flutter_modular.dart'; 5 | import 'package:google_fonts/google_fonts.dart'; 6 | import 'package:knkpanime/navigation.dart'; 7 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 8 | import 'package:knkpanime/utils/utils.dart'; 9 | import 'package:flutter_displaymode/flutter_displaymode.dart'; 10 | 11 | class AppWidget extends StatefulWidget { 12 | const AppWidget({super.key}); 13 | 14 | @override 15 | State createState() => _AppWidgetState(); 16 | } 17 | 18 | class _AppWidgetState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | // 设置高帧率 22 | if (Platform.isAndroid) { 23 | try { 24 | late List modes; 25 | FlutterDisplayMode.supported.then((value) { 26 | modes = value; 27 | DisplayMode f = DisplayMode.auto; 28 | DisplayMode preferred = modes.toList().firstWhere((el) => el == f); 29 | FlutterDisplayMode.setPreferredMode(preferred); 30 | }); 31 | } catch (_) {} 32 | } 33 | return AdaptiveTheme( 34 | light: ThemeData.light(useMaterial3: true).copyWith( 35 | textTheme: Modular.get().useDefaultFont 36 | ? null 37 | : GoogleFonts.notoSerifHkTextTheme()), 38 | dark: ThemeData.dark(useMaterial3: true).copyWith( 39 | textTheme: Modular.get().useDefaultFont 40 | ? null 41 | : GoogleFonts.notoSerifHkTextTheme()), 42 | initial: AdaptiveThemeMode.light, 43 | builder: (theme, darkTheme) => MaterialApp.router( 44 | title: "KNKP Anime", 45 | theme: theme, 46 | darkTheme: darkTheme, 47 | //routeInformationParser: Modular.routeInformationParser, 48 | //routerDelegate: Modular.routerDelegate, 49 | routerConfig: Modular.routerConfig, 50 | builder: (context, child) => Overlay( 51 | initialEntries: [ 52 | OverlayEntry( 53 | builder: (context) => Scaffold( 54 | body: SafeArea( 55 | child: Row( 56 | crossAxisAlignment: CrossAxisAlignment.start, 57 | children: [ 58 | if (!Utils.isSmallScreen(context)) 59 | const SideMenu(), 60 | Expanded( 61 | flex: 5, 62 | child: child!, 63 | ) 64 | ], 65 | ), 66 | ), 67 | bottomNavigationBar: Utils.isSmallScreen(context) 68 | ? const BottomNavigation() 69 | : null, 70 | )) 71 | ], 72 | ))); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:hive_flutter/adapters.dart'; 4 | import 'package:knkpanime/app_module.dart'; 5 | import 'package:knkpanime/app_widget.dart'; 6 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 7 | import 'package:knkpanime/utils/storage.dart'; 8 | import 'package:knkpanime/utils/utils.dart'; 9 | import 'package:media_kit/media_kit.dart'; 10 | import 'package:package_info_plus/package_info_plus.dart'; 11 | import 'package:path_provider/path_provider.dart'; 12 | import 'package:shared_preferences/shared_preferences.dart'; 13 | import 'package:window_manager/window_manager.dart'; 14 | import 'package:flutter/services.dart'; 15 | 16 | late String version; 17 | 18 | void main() async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( 21 | statusBarColor: Colors.transparent, 22 | )); 23 | Modular.setInitialRoute('/search/bangumi'); 24 | await Hive.initFlutter('${(await getApplicationSupportDirectory()).path}/v1'); 25 | await SettingsController.init(); 26 | await Storage.init(); 27 | MediaKit.ensureInitialized(); 28 | version = (await PackageInfo.fromPlatform()).version; 29 | if (Utils.isDesktop()) { 30 | await windowManager.ensureInitialized(); 31 | final prefs = await SharedPreferences.getInstance(); 32 | windowManager.setAlwaysOnTop(prefs.getBool('alwaysOnTop') ?? false); 33 | } 34 | runApp(ModularApp(module: AppModule(), child: const AppWidget())); 35 | } 36 | -------------------------------------------------------------------------------- /lib/models/anime_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | part 'anime_info.g.dart'; 4 | 5 | @HiveType(typeId: 4) 6 | class AnimeInfo { 7 | @HiveField(0) 8 | int id; 9 | @HiveField(1) 10 | String url; 11 | @HiveField(2) 12 | String nameCn; 13 | @HiveField(3) 14 | String nameJp; 15 | @HiveField(4) 16 | String summary; 17 | @HiveField(5) 18 | String airDate; 19 | @HiveField(6) 20 | Map? images; 21 | 22 | AnimeInfo(this.id, this.url, this.nameCn, this.nameJp, this.summary, 23 | this.airDate, this.images); 24 | 25 | String get name => nameCn.isNotEmpty ? nameCn : nameJp; 26 | 27 | @override 28 | String toString() { 29 | return 'id: $id\n' 30 | 'url: $url\n' 31 | 'Chinese name: $nameCn\n' 32 | 'Japanese name: $nameJp'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/models/anime_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'anime_info.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class AnimeInfoAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 4; 12 | 13 | @override 14 | AnimeInfo read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return AnimeInfo( 20 | fields[0] as int, 21 | fields[1] as String, 22 | fields[2] as String, 23 | fields[3] as String, 24 | fields[4] as String, 25 | fields[5] as String, 26 | (fields[6] as Map?)?.cast(), 27 | ); 28 | } 29 | 30 | @override 31 | void write(BinaryWriter writer, AnimeInfo obj) { 32 | writer 33 | ..writeByte(7) 34 | ..writeByte(0) 35 | ..write(obj.id) 36 | ..writeByte(1) 37 | ..write(obj.url) 38 | ..writeByte(2) 39 | ..write(obj.nameCn) 40 | ..writeByte(3) 41 | ..write(obj.nameJp) 42 | ..writeByte(4) 43 | ..write(obj.summary) 44 | ..writeByte(5) 45 | ..write(obj.airDate) 46 | ..writeByte(6) 47 | ..write(obj.images); 48 | } 49 | 50 | @override 51 | int get hashCode => typeId.hashCode; 52 | 53 | @override 54 | bool operator ==(Object other) => 55 | identical(this, other) || 56 | other is AnimeInfoAdapter && 57 | runtimeType == other.runtimeType && 58 | typeId == other.typeId; 59 | } 60 | -------------------------------------------------------------------------------- /lib/models/danmaku.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:ns_danmaku/ns_danmaku.dart'; 3 | 4 | class Danmaku { 5 | String content; 6 | double offset; 7 | // TODO: support danmaku position & color? 8 | DanmakuItemType position; 9 | Color color; 10 | 11 | Danmaku(this.offset, this.content, this.position, this.color); 12 | } 13 | 14 | class DanmakuAnimeInfo { 15 | int id; 16 | String name; 17 | int episodeCount; 18 | 19 | DanmakuAnimeInfo(this.id, this.name, this.episodeCount); 20 | } 21 | -------------------------------------------------------------------------------- /lib/models/episode.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | part 'episode.g.dart'; 4 | 5 | @HiveType(typeId: 2) 6 | class Episode { 7 | /// Adapter defined episode id, this will later be used to play the video 8 | @HiveField(0) 9 | String episodeId; 10 | 11 | /// This field must be the index of the episode, starting from 0 12 | /// This will be used to construct watching history 13 | @HiveField(1) 14 | int episode; 15 | 16 | /// Adapter defined episode name, will be displayed on the playlist. 17 | @HiveField(2) 18 | String? episodeName; 19 | 20 | String get name => episodeName ?? '第${episode + 1}集'; 21 | 22 | Episode(this.episodeId, this.episode, [this.episodeName]); 23 | 24 | Episode.fromDynamicJson(dynamic json) 25 | : episodeId = json['episodeId']!, 26 | episode = json['episode']!, 27 | episodeName = json['episodeName']; 28 | } 29 | -------------------------------------------------------------------------------- /lib/models/episode.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'episode.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class EpisodeAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 2; 12 | 13 | @override 14 | Episode read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return Episode( 20 | fields[0] as String, 21 | fields[1] as int, 22 | fields[2] as String?, 23 | ); 24 | } 25 | 26 | @override 27 | void write(BinaryWriter writer, Episode obj) { 28 | writer 29 | ..writeByte(3) 30 | ..writeByte(0) 31 | ..write(obj.episodeId) 32 | ..writeByte(1) 33 | ..write(obj.episode) 34 | ..writeByte(2) 35 | ..write(obj.episodeName); 36 | } 37 | 38 | @override 39 | int get hashCode => typeId.hashCode; 40 | 41 | @override 42 | bool operator ==(Object other) => 43 | identical(this, other) || 44 | other is EpisodeAdapter && 45 | runtimeType == other.runtimeType && 46 | typeId == other.typeId; 47 | } 48 | -------------------------------------------------------------------------------- /lib/models/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | import 'package:knkpanime/models/episode.dart'; 3 | import 'package:knkpanime/models/series.dart'; 4 | 5 | part 'history.g.dart'; 6 | 7 | @HiveType(typeId: 0) 8 | class History { 9 | @HiveField(0) 10 | Map progresses = {}; 11 | 12 | @HiveField(1) 13 | int lastWatchEpisode; 14 | 15 | @HiveField(2) 16 | String adapterName; 17 | 18 | @HiveField(3) 19 | Series series; 20 | 21 | @HiveField(4) 22 | DateTime lastWatchTime; 23 | 24 | String get key => adapterName + series.seriesId; 25 | 26 | History( 27 | this.series, this.lastWatchEpisode, this.adapterName, this.lastWatchTime); 28 | 29 | static String getKey(String n, Series s) => n + s.seriesId; 30 | 31 | @override 32 | String toString() { 33 | return 'Adapter: $adapterName, anime: $series'; 34 | } 35 | } 36 | 37 | @HiveType(typeId: 1) 38 | class Progress { 39 | @HiveField(0) 40 | Episode episode; 41 | 42 | @HiveField(1) 43 | int _progressInMilli; 44 | 45 | Duration get progress => Duration(milliseconds: _progressInMilli); 46 | 47 | set progress(Duration d) => _progressInMilli = d.inMilliseconds; 48 | 49 | Progress(this.episode, this._progressInMilli); 50 | 51 | @override 52 | String toString() { 53 | return 'Episode $episode, progress $progress'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/models/history.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'history.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class HistoryAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 0; 12 | 13 | @override 14 | History read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return History( 20 | fields[3] as Series, 21 | fields[1] as int, 22 | fields[2] as String, 23 | fields[4] as DateTime, 24 | )..progresses = (fields[0] as Map).cast(); 25 | } 26 | 27 | @override 28 | void write(BinaryWriter writer, History obj) { 29 | writer 30 | ..writeByte(5) 31 | ..writeByte(0) 32 | ..write(obj.progresses) 33 | ..writeByte(1) 34 | ..write(obj.lastWatchEpisode) 35 | ..writeByte(2) 36 | ..write(obj.adapterName) 37 | ..writeByte(3) 38 | ..write(obj.series) 39 | ..writeByte(4) 40 | ..write(obj.lastWatchTime); 41 | } 42 | 43 | @override 44 | int get hashCode => typeId.hashCode; 45 | 46 | @override 47 | bool operator ==(Object other) => 48 | identical(this, other) || 49 | other is HistoryAdapter && 50 | runtimeType == other.runtimeType && 51 | typeId == other.typeId; 52 | } 53 | 54 | class ProgressAdapter extends TypeAdapter { 55 | @override 56 | final int typeId = 1; 57 | 58 | @override 59 | Progress read(BinaryReader reader) { 60 | final numOfFields = reader.readByte(); 61 | final fields = { 62 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 63 | }; 64 | return Progress( 65 | fields[0] as Episode, 66 | fields[1] as int, 67 | ); 68 | } 69 | 70 | @override 71 | void write(BinaryWriter writer, Progress obj) { 72 | writer 73 | ..writeByte(2) 74 | ..writeByte(0) 75 | ..write(obj.episode) 76 | ..writeByte(1) 77 | ..write(obj._progressInMilli); 78 | } 79 | 80 | @override 81 | int get hashCode => typeId.hashCode; 82 | 83 | @override 84 | bool operator ==(Object other) => 85 | identical(this, other) || 86 | other is ProgressAdapter && 87 | runtimeType == other.runtimeType && 88 | typeId == other.typeId; 89 | } 90 | -------------------------------------------------------------------------------- /lib/models/image_set.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:hive/hive.dart'; 4 | 5 | part 'image_set.g.dart'; 6 | 7 | @HiveType(typeId: 5) 8 | class ImageSet { 9 | @HiveField(0) 10 | Uint8List sideMenuBackground; 11 | @HiveField(1) 12 | Uint8List coverPlaceholder; 13 | @HiveField(2) 14 | Uint8List coverNoImage; 15 | 16 | ImageSet(this.sideMenuBackground, this.coverPlaceholder, this.coverNoImage) {} 17 | } 18 | -------------------------------------------------------------------------------- /lib/models/image_set.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'image_set.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class ImageSetAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 5; 12 | 13 | @override 14 | ImageSet read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return ImageSet( 20 | fields[0] as Uint8List, 21 | fields[1] as Uint8List, 22 | fields[2] as Uint8List, 23 | ); 24 | } 25 | 26 | @override 27 | void write(BinaryWriter writer, ImageSet obj) { 28 | writer 29 | ..writeByte(3) 30 | ..writeByte(0) 31 | ..write(obj.sideMenuBackground) 32 | ..writeByte(1) 33 | ..write(obj.coverPlaceholder) 34 | ..writeByte(2) 35 | ..write(obj.coverNoImage); 36 | } 37 | 38 | @override 39 | int get hashCode => typeId.hashCode; 40 | 41 | @override 42 | bool operator ==(Object other) => 43 | identical(this, other) || 44 | other is ImageSetAdapter && 45 | runtimeType == other.runtimeType && 46 | typeId == other.typeId; 47 | } 48 | -------------------------------------------------------------------------------- /lib/models/series.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | part 'series.g.dart'; 4 | 5 | @HiveType(typeId: 3) 6 | class Series { 7 | /// Anime name 8 | @HiveField(0) 9 | String name; 10 | 11 | /// Anime series id, defined by each adapter and will later be used to 12 | /// fetch detailed info. 13 | @HiveField(1) 14 | String seriesId; 15 | 16 | /// Optional description for the anime. If not null, this will be 17 | /// displayed with the title on the source selection window. 18 | @HiveField(2) 19 | String? description; 20 | 21 | /// Optional thumbnail image url. If not null, this will be displayed 22 | /// on the history page 23 | @HiveField(3) 24 | String? image; 25 | 26 | Series(this.seriesId, this.name, {this.description, this.image}); 27 | 28 | Series.fromDynamicJson(dynamic json) 29 | : seriesId = json['seriesId']!, 30 | name = json['name']!, 31 | description = json['description'], 32 | image = json['image']; 33 | 34 | @override 35 | String toString() { 36 | return 'Name: $name\nId: $seriesId\nDescription: $description'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/models/series.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'series.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class SeriesAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 3; 12 | 13 | @override 14 | Series read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return Series( 20 | fields[1] as String, 21 | fields[0] as String, 22 | description: fields[2] as String?, 23 | image: fields[3] as String?, 24 | ); 25 | } 26 | 27 | @override 28 | void write(BinaryWriter writer, Series obj) { 29 | writer 30 | ..writeByte(4) 31 | ..writeByte(0) 32 | ..write(obj.name) 33 | ..writeByte(1) 34 | ..write(obj.seriesId) 35 | ..writeByte(2) 36 | ..write(obj.description) 37 | ..writeByte(3) 38 | ..write(obj.image); 39 | } 40 | 41 | @override 42 | int get hashCode => typeId.hashCode; 43 | 44 | @override 45 | bool operator ==(Object other) => 46 | identical(this, other) || 47 | other is SeriesAdapter && 48 | runtimeType == other.runtimeType && 49 | typeId == other.typeId; 50 | } 51 | -------------------------------------------------------------------------------- /lib/models/source.dart: -------------------------------------------------------------------------------- 1 | import 'package:knkpanime/models/episode.dart'; 2 | 3 | class Source { 4 | List episodes; 5 | String? sourceName; 6 | 7 | Source(this.episodes, [this.sourceName]); 8 | 9 | Source.fromDynamicJson(dynamic json) 10 | : episodes = (json['episodes'] as List) 11 | .map((e) => Episode.fromDynamicJson(e)) 12 | .toList(), 13 | sourceName = json['sourceName']; 14 | } 15 | -------------------------------------------------------------------------------- /lib/pages/calendar/calendar_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:knkpanime/models/anime_info.dart'; 2 | import 'package:knkpanime/utils/bangumi.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'calendar_controller.g.dart'; 6 | 7 | class CalendarController = _CalendarController with _$CalendarController; 8 | 9 | abstract class _CalendarController with Store { 10 | @observable 11 | List> animeList = List.filled(7, []); 12 | 13 | var lastUpdatedTime = DateTime.now(); 14 | 15 | bool _updatedCalendar() { 16 | DateTime currentTime = DateTime.now(); 17 | DateTime currentTimeUTC8 = currentTime.toUtc().add(Duration(hours: 8)); 18 | 19 | return lastUpdatedTime.day == currentTime.day && 20 | lastUpdatedTime.day == currentTimeUTC8.day; 21 | } 22 | 23 | Future update() async { 24 | animeList = await Bangumi.fetchTodayAnime(); 25 | } 26 | 27 | _CalendarController() { 28 | update(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/pages/calendar/calendar_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'calendar_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // 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 10 | 11 | mixin _$CalendarController on _CalendarController, Store { 12 | late final _$animeListAtom = 13 | Atom(name: '_CalendarController.animeList', context: context); 14 | 15 | @override 16 | List> get animeList { 17 | _$animeListAtom.reportRead(); 18 | return super.animeList; 19 | } 20 | 21 | @override 22 | set animeList(List> value) { 23 | _$animeListAtom.reportWrite(value, super.animeList, () { 24 | super.animeList = value; 25 | }); 26 | } 27 | 28 | @override 29 | String toString() { 30 | return ''' 31 | animeList: ${animeList} 32 | '''; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/pages/calendar/calendar_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/calendar/calendar_page.dart'; 3 | 4 | class CalendarModule extends Module { 5 | @override 6 | void binds(Injector i) {} 7 | 8 | @override 9 | void routes(RouteManager r) { 10 | r.child('/', child: (context) => TodayPage()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/pages/calendar/calendar_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_modular/flutter_modular.dart'; 4 | import 'package:knkpanime/pages/calendar/calendar_controller.dart'; 5 | import 'package:knkpanime/widgets/anime_card.dart'; 6 | import 'package:knkpanime/widgets/source_selection_window.dart'; 7 | import 'package:logger/logger.dart'; 8 | 9 | // Deprecated page 10 | 11 | class TodayPage extends StatefulWidget { 12 | const TodayPage({super.key}); 13 | 14 | @override 15 | State createState() => _TodayPageState(); 16 | } 17 | 18 | class _TodayPageState extends State 19 | with SingleTickerProviderStateMixin { 20 | final calendarController = Modular.get(); 21 | late TabController _tabController; 22 | final weekdayNameMap = [ 23 | '星期一', 24 | '星期二', 25 | '星期三', 26 | '星期四', 27 | '星期五', 28 | '星期六', 29 | '星期日', 30 | ]; 31 | var selectedDay = DateTime.now().weekday - 1; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | calendarController.update(); 37 | _tabController = TabController(length: 7, vsync: this); 38 | _tabController.index = selectedDay; 39 | _tabController.addListener(() { 40 | setState(() { 41 | selectedDay = _tabController.index; 42 | }); 43 | }); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Observer( 49 | builder: (_) => Column( 50 | children: [ 51 | TabBar( 52 | controller: _tabController, 53 | tabs: weekdayNameMap.map((e) => Text(e)).toList()), 54 | Expanded( 55 | child: TabBarView( 56 | controller: _tabController, 57 | children: calendarController.animeList 58 | .map((animes) => ListView( 59 | children: animes 60 | .map((anime) => AnimeCard(anime, (anime) { 61 | showDialog( 62 | context: context, 63 | builder: (context) => 64 | SourceSelectionWindow( 65 | anime: anime, 66 | searchKeyword: '', 67 | onSearchResultTap: 68 | (adapter, res) { 69 | Modular.get().i( 70 | 'Selected resource: ${res.toString()}'); 71 | Modular.to.pushNamed( 72 | '/play/', 73 | arguments: { 74 | 'adapter': adapter, 75 | 'series': res, 76 | }); 77 | }, 78 | )); 79 | })) 80 | .toList(), 81 | )) 82 | .toList(), 83 | ), 84 | ) 85 | ], 86 | )); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/pages/favorite/favorite_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:knkpanime/models/anime_info.dart'; 2 | import 'package:knkpanime/utils/storage.dart'; 3 | 4 | class FavoriteController { 5 | late var storedFavorites = Storage.favorites; 6 | 7 | List get favorites => storedFavorites.values.toList(); 8 | 9 | bool isFavorite(AnimeInfo anime) { 10 | return !(storedFavorites.get(anime.id) == null); 11 | } 12 | 13 | void addFavorite(AnimeInfo anime) { 14 | storedFavorites.put(anime.id, anime); 15 | } 16 | 17 | void deleteFavorite(AnimeInfo anime) { 18 | storedFavorites.delete(anime.id); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/pages/favorite/favorite_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/favorite/favorite_page.dart'; 3 | 4 | class FavoriteModule extends Module { 5 | @override 6 | void routes(RouteManager r) { 7 | r.child('/', child: (_) => FavoritePage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/pages/favorite/favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/models/anime_info.dart'; 4 | import 'package:knkpanime/pages/favorite/favorite_controller.dart'; 5 | import 'package:knkpanime/utils/utils.dart'; 6 | import 'package:knkpanime/widgets/anime_card.dart'; 7 | import 'package:knkpanime/widgets/source_selection_window.dart'; 8 | import 'package:logger/logger.dart'; 9 | 10 | class FavoritePage extends StatefulWidget { 11 | const FavoritePage({super.key}); 12 | 13 | @override 14 | State createState() => _FavoritePageState(); 15 | } 16 | 17 | class _FavoritePageState extends State { 18 | late var favoriteController = Modular.get(); 19 | List favorites = []; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | favorites = favoriteController.favorites; 25 | setState(() {}); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | appBar: AppBar( 32 | leading: const Icon(Icons.search), 33 | elevation: 2, 34 | title: TextField( 35 | autofocus: Utils.isDesktop(), 36 | decoration: const InputDecoration( 37 | hintText: '搜索', 38 | border: InputBorder.none, 39 | ), 40 | onSubmitted: (value) { 41 | favorites = favoriteController.favorites 42 | .where((element) => element.name.contains(value)) 43 | .toList(); 44 | setState(() {}); 45 | }, 46 | ), 47 | ), 48 | body: SafeArea( 49 | child: ListView( 50 | shrinkWrap: true, 51 | children: favorites 52 | .map( 53 | (anime) => AnimeCard(anime, (anime) { 54 | Modular.get() 55 | .i('Clicked in favorite page: \n${anime.name}'); 56 | showDialog( 57 | context: context, 58 | builder: (context) => SourceSelectionWindow( 59 | anime: anime, 60 | searchKeyword: '', 61 | onSearchResultTap: (adapter, res) { 62 | Modular.get() 63 | .i('Selected resource: ${res.toString()}'); 64 | Modular.to.pushNamed('/play/', arguments: { 65 | 'adapter': adapter, 66 | 'series': res, 67 | }); 68 | }, 69 | )); 70 | }), 71 | ) 72 | .toList(), 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/pages/history/history_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/models/episode.dart'; 3 | import 'package:knkpanime/models/series.dart'; 4 | import 'package:knkpanime/models/history.dart'; 5 | import 'package:knkpanime/utils/storage.dart'; 6 | import 'package:logger/logger.dart'; 7 | 8 | class HistoryController { 9 | late var storedHistories = Storage.histories; 10 | 11 | List get histories { 12 | var temp = storedHistories.values.toList(); 13 | temp.sort( 14 | (a, b) => 15 | b.lastWatchTime.millisecondsSinceEpoch - 16 | a.lastWatchTime.millisecondsSinceEpoch, 17 | ); 18 | return temp; 19 | } 20 | 21 | void updateHistory( 22 | Episode episode, String adapterName, Series series, Duration progress) { 23 | var history = storedHistories.get(History.getKey(adapterName, series)) ?? 24 | History(series, episode.episode, adapterName, DateTime.now()); 25 | history.lastWatchEpisode = episode.episode; 26 | history.lastWatchTime = DateTime.now(); 27 | 28 | var prog = history.progresses[episode.episode]; 29 | if (prog == null) { 30 | history.progresses[episode.episode] = 31 | Progress(episode, progress.inMilliseconds); 32 | } else { 33 | prog.progress = progress; 34 | } 35 | 36 | storedHistories.put(history.key, history); 37 | } 38 | 39 | Progress? lastWatching(Series series, String adapterName) { 40 | var history = storedHistories.get(History.getKey(adapterName, series)); 41 | return history?.progresses[history.lastWatchEpisode]; 42 | } 43 | 44 | Progress? findProgress(Series series, String adapterName, int episode) { 45 | var history = storedHistories.get(History.getKey(adapterName, series)); 46 | return history?.progresses[episode]; 47 | } 48 | 49 | void deleteHistory(History history) { 50 | storedHistories.delete(history.key); 51 | } 52 | 53 | void clearProgress(Series series, String adapterName, int episode) { 54 | var history = storedHistories.get(History.getKey(adapterName, series)); 55 | history!.progresses[episode]!.progress = Duration.zero; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/pages/history/history_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/history/history_page.dart'; 3 | 4 | class HistoryModule extends Module { 5 | @override 6 | void routes(RouteManager r) { 7 | r.child('/', child: (_) => HistoryPage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/pages/history/history_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/adapters/adapter_registry.dart'; 4 | import 'package:knkpanime/pages/history/history_controller.dart'; 5 | import 'package:knkpanime/pages/search/adapter_search_controller.dart'; 6 | import 'package:knkpanime/widgets/series_card.dart'; 7 | import 'package:logger/logger.dart'; 8 | 9 | class HistoryPage extends StatefulWidget { 10 | const HistoryPage({super.key}); 11 | 12 | @override 13 | State createState() => _HistoryPageState(); 14 | } 15 | 16 | class _HistoryPageState extends State { 17 | late var historyController = Modular.get(); 18 | late final adapterSearchController = Modular.get(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | body: SafeArea( 24 | child: ListView.builder( 25 | shrinkWrap: true, 26 | itemCount: historyController.histories.length, 27 | itemBuilder: (context, index) { 28 | var history = historyController.histories[index]; 29 | return SeriesCard( 30 | history.series, 31 | history.progresses[history.lastWatchEpisode]!, 32 | (anime) { 33 | Modular.get() 34 | .i('Selected history:\n${history.series.toString()}'); 35 | 36 | Modular.to.pushNamed('/play/', arguments: { 37 | 'adapter': adapterSearchController.availableAdapters 38 | .where((element) => element.name == history.adapterName) 39 | .first, 40 | 'series': history.series, 41 | }).then((_) => setState(() {})); 42 | }, 43 | history.adapterName, 44 | onDelete: () { 45 | historyController.deleteHistory(history); 46 | setState(() {}); 47 | }, 48 | ); 49 | }, 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/pages/play/play_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/play/play_page.dart'; 3 | 4 | class PlayModule extends Module { 5 | @override 6 | void routes(RouteManager r) { 7 | r.child('/', 8 | child: (context) => PlayPage( 9 | adapter: r.args.data['adapter'], series: r.args.data['series'])); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/pages/search/adapter_search_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_modular/flutter_modular.dart'; 6 | import 'package:knkpanime/adapters/adapter_base.dart'; 7 | import 'package:knkpanime/adapters/adapter_registry.dart' as registry; 8 | import 'package:knkpanime/adapters/js_adapter.dart'; 9 | import 'package:knkpanime/models/series.dart'; 10 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 11 | import 'package:logger/logger.dart'; 12 | import 'package:mobx/mobx.dart'; 13 | 14 | part 'adapter_search_controller.g.dart'; 15 | 16 | class AdapterSearchController = _AdapterSearchController 17 | with _$AdapterSearchController; 18 | 19 | abstract class _AdapterSearchController with Store { 20 | @observable 21 | ObservableList> _searchResults = 22 | ObservableList.of(registry.adapters.map((e) => [])); 23 | @observable 24 | var _statuses = 25 | ObservableList.of(registry.adapters.map((adapter) => adapter.status)); 26 | @computed 27 | List> get searchResults => _searchResults 28 | .where((result) => 29 | _adapterAvailable(_adapters[_searchResults.indexOf(result)])) 30 | .toList(); 31 | @computed 32 | List get statuses => _statuses 33 | .where( 34 | (status) => _adapterAvailable(_adapters[_statuses.indexOf(status)])) 35 | .toList(); 36 | 37 | @observable 38 | var _adapters = ObservableList.of(registry.adapters); 39 | 40 | List get availableAdapters => 41 | _adapters.where((adapter) => _adapterAvailable(adapter)).toList(); 42 | 43 | @computed 44 | List get jsAdapters => _adapters.whereType().toList(); 45 | 46 | late final settingsController = Modular.get(); 47 | 48 | void search(String bangumiName, String keyword) { 49 | if (bangumiName.isEmpty && keyword.isEmpty) { 50 | _searchResults.clear(); 51 | _searchResults = ObservableList.of(_adapters.map((e) => [])); 52 | return; 53 | } 54 | _adapters.asMap().forEach((idx, adapter) async { 55 | try { 56 | var future = adapter.search(bangumiName, keyword); 57 | _statuses[idx] = adapter.status; 58 | _searchResults[idx] = []; 59 | _searchResults[idx] = await future; 60 | } catch (e) { 61 | Modular.get().w(e); 62 | } 63 | _statuses[idx] = adapter.status; 64 | }); 65 | } 66 | 67 | void clear() { 68 | _searchResults.clear(); 69 | _searchResults = ObservableList.of(_adapters.map((e) => [])); 70 | } 71 | 72 | void addJsAdapter(String sourceUrl) async { 73 | if (jsAdapters.map((adapter) => adapter.sourceUrl).contains(sourceUrl)) { 74 | Modular.get() 75 | .i('JS Adapter from source $sourceUrl already exists, aborting'); 76 | return; 77 | } 78 | Modular.get().i('Initializing js adapter from $sourceUrl'); 79 | final adapter = JSAdapter(sourceUrl); 80 | await adapter.completer.future; 81 | Modular.get().i('Initialized, success: ${adapter.initialized}'); 82 | if (adapter.initialized && 83 | !_adapters.map((adapter) => adapter.name).contains(adapter.name)) { 84 | _adapters.add(adapter); 85 | _statuses.add(adapter.status); 86 | _searchResults.add([]); 87 | final stored = settingsController.jsAdapters; 88 | if (!stored.contains(sourceUrl)) { 89 | stored.add(sourceUrl); 90 | settingsController.jsAdapters = stored; 91 | } 92 | } 93 | } 94 | 95 | void removeJsAdapter(JSAdapter adapter) { 96 | _adapters.remove(adapter); 97 | final stored = settingsController.jsAdapters; 98 | stored.removeWhere((element) => adapter.sourceUrl == element); 99 | settingsController.jsAdapters = stored; 100 | } 101 | 102 | bool _adapterAvailable(AdapterBase adapter) { 103 | return (!adapter.useWebview || settingsController.useWebViewAdapters) && 104 | (adapter is! JSAdapter || adapter.initialized); 105 | } 106 | 107 | _AdapterSearchController() { 108 | Dio() 109 | .get( 110 | 'https://raw.githubusercontent.com/KNKPA/KNKPAnime-js-adapters/main/registry.json') 111 | .then((resp) => 112 | (jsonDecode(resp.data) as List).forEach((url) => addJsAdapter(url))) 113 | .whenComplete(() => 114 | settingsController.jsAdapters.forEach((url) => addJsAdapter(url))); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/pages/search/adapter_search_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'adapter_search_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // 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 10 | 11 | mixin _$AdapterSearchController on _AdapterSearchController, Store { 12 | Computed>>? _$searchResultsComputed; 13 | 14 | @override 15 | List> get searchResults => (_$searchResultsComputed ??= 16 | Computed>>(() => super.searchResults, 17 | name: '_AdapterSearchController.searchResults')) 18 | .value; 19 | Computed>? _$statusesComputed; 20 | 21 | @override 22 | List get statuses => 23 | (_$statusesComputed ??= Computed>(() => super.statuses, 24 | name: '_AdapterSearchController.statuses')) 25 | .value; 26 | Computed>? _$jsAdaptersComputed; 27 | 28 | @override 29 | List get jsAdapters => (_$jsAdaptersComputed ??= 30 | Computed>(() => super.jsAdapters, 31 | name: '_AdapterSearchController.jsAdapters')) 32 | .value; 33 | 34 | late final _$_searchResultsAtom = 35 | Atom(name: '_AdapterSearchController._searchResults', context: context); 36 | 37 | @override 38 | ObservableList> get _searchResults { 39 | _$_searchResultsAtom.reportRead(); 40 | return super._searchResults; 41 | } 42 | 43 | @override 44 | set _searchResults(ObservableList> value) { 45 | _$_searchResultsAtom.reportWrite(value, super._searchResults, () { 46 | super._searchResults = value; 47 | }); 48 | } 49 | 50 | late final _$_statusesAtom = 51 | Atom(name: '_AdapterSearchController._statuses', context: context); 52 | 53 | @override 54 | ObservableList get _statuses { 55 | _$_statusesAtom.reportRead(); 56 | return super._statuses; 57 | } 58 | 59 | @override 60 | set _statuses(ObservableList value) { 61 | _$_statusesAtom.reportWrite(value, super._statuses, () { 62 | super._statuses = value; 63 | }); 64 | } 65 | 66 | late final _$_adaptersAtom = 67 | Atom(name: '_AdapterSearchController._adapters', context: context); 68 | 69 | @override 70 | ObservableList get _adapters { 71 | _$_adaptersAtom.reportRead(); 72 | return super._adapters; 73 | } 74 | 75 | @override 76 | set _adapters(ObservableList value) { 77 | _$_adaptersAtom.reportWrite(value, super._adapters, () { 78 | super._adapters = value; 79 | }); 80 | } 81 | 82 | @override 83 | String toString() { 84 | return ''' 85 | searchResults: ${searchResults}, 86 | statuses: ${statuses}, 87 | jsAdapters: ${jsAdapters} 88 | '''; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/pages/search/bangumi_search_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/models/anime_info.dart'; 4 | import 'package:knkpanime/utils/bangumi.dart'; 5 | import 'package:logger/logger.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'bangumi_search_controller.g.dart'; 9 | 10 | class BangumiSearchController = _BangumiSearchController 11 | with _$BangumiSearchController; 12 | 13 | abstract class _BangumiSearchController with Store { 14 | @observable 15 | var searchResults = ObservableList(); 16 | @observable 17 | bool searching = false; 18 | @observable 19 | int _items = 0; 20 | @observable 21 | int _page = 0; 22 | @computed 23 | bool get hasMore => _page * _maxResults < _items; 24 | @observable 25 | bool failed = false; 26 | 27 | String _keyword = ''; 28 | String get keyword => _keyword; 29 | final int _maxResults = 25; 30 | 31 | void search(String keyword) async { 32 | if (keyword == _keyword && !failed) return; 33 | clear(); 34 | if (keyword.isEmpty) return; 35 | 36 | _keyword = keyword; 37 | searchResults.addAll(await _search()); 38 | } 39 | 40 | void loadMore() async { 41 | searchResults.addAll(await _search()); 42 | debugPrint('$_page $_items'); 43 | } 44 | 45 | void clear() { 46 | searchResults.clear(); 47 | _page = 0; 48 | _keyword = ''; 49 | _items = 0; 50 | failed = false; 51 | } 52 | 53 | Future> _search() async { 54 | searching = true; 55 | try { 56 | var (animes, totalItems) = await Bangumi.search(_keyword, 57 | start: _page * _maxResults, maxResults: _maxResults); 58 | _items = totalItems; 59 | _page++; 60 | return animes; 61 | } catch (e) { 62 | failed = true; 63 | Modular.get().w(e.toString()); 64 | } finally { 65 | searching = false; 66 | } 67 | return []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/pages/search/bangumi_search_controller.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'bangumi_search_controller.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // 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 10 | 11 | mixin _$BangumiSearchController on _BangumiSearchController, Store { 12 | Computed? _$hasMoreComputed; 13 | 14 | @override 15 | bool get hasMore => (_$hasMoreComputed ??= Computed(() => super.hasMore, 16 | name: '_BangumiSearchController.hasMore')) 17 | .value; 18 | 19 | late final _$searchResultsAtom = 20 | Atom(name: '_BangumiSearchController.searchResults', context: context); 21 | 22 | @override 23 | ObservableList get searchResults { 24 | _$searchResultsAtom.reportRead(); 25 | return super.searchResults; 26 | } 27 | 28 | @override 29 | set searchResults(ObservableList value) { 30 | _$searchResultsAtom.reportWrite(value, super.searchResults, () { 31 | super.searchResults = value; 32 | }); 33 | } 34 | 35 | late final _$searchingAtom = 36 | Atom(name: '_BangumiSearchController.searching', context: context); 37 | 38 | @override 39 | bool get searching { 40 | _$searchingAtom.reportRead(); 41 | return super.searching; 42 | } 43 | 44 | @override 45 | set searching(bool value) { 46 | _$searchingAtom.reportWrite(value, super.searching, () { 47 | super.searching = value; 48 | }); 49 | } 50 | 51 | late final _$_itemsAtom = 52 | Atom(name: '_BangumiSearchController._items', context: context); 53 | 54 | @override 55 | int get _items { 56 | _$_itemsAtom.reportRead(); 57 | return super._items; 58 | } 59 | 60 | @override 61 | set _items(int value) { 62 | _$_itemsAtom.reportWrite(value, super._items, () { 63 | super._items = value; 64 | }); 65 | } 66 | 67 | late final _$_pageAtom = 68 | Atom(name: '_BangumiSearchController._page', context: context); 69 | 70 | @override 71 | int get _page { 72 | _$_pageAtom.reportRead(); 73 | return super._page; 74 | } 75 | 76 | @override 77 | set _page(int value) { 78 | _$_pageAtom.reportWrite(value, super._page, () { 79 | super._page = value; 80 | }); 81 | } 82 | 83 | late final _$failedAtom = 84 | Atom(name: '_BangumiSearchController.failed', context: context); 85 | 86 | @override 87 | bool get failed { 88 | _$failedAtom.reportRead(); 89 | return super.failed; 90 | } 91 | 92 | @override 93 | set failed(bool value) { 94 | _$failedAtom.reportWrite(value, super.failed, () { 95 | super.failed = value; 96 | }); 97 | } 98 | 99 | @override 100 | String toString() { 101 | return ''' 102 | searchResults: ${searchResults}, 103 | searching: ${searching}, 104 | failed: ${failed}, 105 | hasMore: ${hasMore} 106 | '''; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/pages/search/bangumi_search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide SearchController; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_modular/flutter_modular.dart'; 4 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 5 | import 'package:knkpanime/utils/utils.dart'; 6 | import 'package:knkpanime/widgets/anime_card.dart'; 7 | import 'package:knkpanime/widgets/source_selection_window.dart'; 8 | import 'package:logger/logger.dart'; 9 | import 'package:url_launcher/url_launcher_string.dart'; 10 | import 'bangumi_search_controller.dart'; 11 | 12 | class BangumiSearchPage extends StatefulWidget { 13 | const BangumiSearchPage({super.key}); 14 | 15 | @override 16 | State createState() => _BangumiSearchPageState(); 17 | } 18 | 19 | class _BangumiSearchPageState extends State { 20 | final BangumiSearchController bangumiSearchController = 21 | Modular.get(); 22 | final logger = Modular.get(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | leading: const Icon(Icons.search), 29 | elevation: 2, 30 | title: TextField( 31 | autofocus: Utils.isDesktop(), 32 | decoration: const InputDecoration( 33 | hintText: '搜索', 34 | border: InputBorder.none, 35 | ), 36 | onSubmitted: (value) { 37 | bangumiSearchController.search(value); 38 | }, 39 | ), 40 | ), 41 | body: SafeArea( 42 | child: Observer(builder: (context) { 43 | if (bangumiSearchController.failed) { 44 | return const Center( 45 | child: Text('搜索失败(可能是由于Bangumi API导致,可以尝试换关键词搜索)'), 46 | ); 47 | } else { 48 | return ListView.builder( 49 | shrinkWrap: true, 50 | itemCount: bangumiSearchController.searchResults.length + 51 | (bangumiSearchController.hasMore ? 1 : 0), 52 | itemBuilder: (context, index) { 53 | if (index == bangumiSearchController.searchResults.length) { 54 | if (bangumiSearchController.hasMore) { 55 | bangumiSearchController.loadMore(); 56 | } 57 | return const Center(child: CircularProgressIndicator()); 58 | } 59 | final anime = bangumiSearchController.searchResults[index]; 60 | return AnimeCard(anime, (anime) { 61 | logger.i('Clicked in search page: \n${anime.name}'); 62 | showDialog( 63 | context: context, 64 | builder: (context) => SourceSelectionWindow( 65 | anime: anime, 66 | searchKeyword: bangumiSearchController.keyword, 67 | onSearchResultTap: (adapter, res) { 68 | Modular.get() 69 | .i('Selected resource: ${res.toString()}'); 70 | Modular.to.pushNamed('/play/', arguments: { 71 | 'adapter': adapter, 72 | 'series': res, 73 | }); 74 | }, 75 | )); 76 | }); 77 | }, 78 | ); 79 | } 80 | }), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/pages/search/search_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/search/adapter_search_page.dart'; 3 | import 'package:knkpanime/pages/search/bangumi_search_page.dart'; 4 | 5 | class SearchModule extends Module { 6 | @override 7 | void routes(RouteManager r) { 8 | r.child('/bangumi', child: (_) => BangumiSearchPage()); 9 | r.child('/adapter', child: (_) => AdapterSearchPage()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/pages/settings/js_adapter_config_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_modular/flutter_modular.dart'; 4 | import 'package:knkpanime/pages/search/adapter_search_controller.dart'; 5 | import 'package:logger/logger.dart'; 6 | import 'package:url_launcher/url_launcher_string.dart'; 7 | 8 | class JsAdapterConfigPage extends StatefulWidget { 9 | const JsAdapterConfigPage({super.key}); 10 | 11 | @override 12 | State createState() => _JsAdapterConfigPageState(); 13 | } 14 | 15 | class _JsAdapterConfigPageState extends State { 16 | late final adapterSearchController = Modular.get(); 17 | final TextEditingController textEditingController = TextEditingController(); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: const Text('管理JavaScript适配器'), 24 | leading: IconButton( 25 | icon: const Icon(Icons.arrow_back), 26 | onPressed: () { 27 | Modular.to.pop(); 28 | }, 29 | ), 30 | ), 31 | body: Column( 32 | children: [ 33 | Padding( 34 | padding: const EdgeInsets.only(left: 16, right: 16), 35 | child: TextField( 36 | controller: textEditingController, 37 | decoration: const InputDecoration( 38 | labelText: '添加适配器URL', 39 | ), 40 | onSubmitted: (url) { 41 | try { 42 | adapterSearchController.addJsAdapter(url); 43 | textEditingController.clear(); 44 | } catch (e) { 45 | Modular.get().w(e); 46 | ScaffoldMessenger.of(context).showSnackBar( 47 | SnackBar(content: Text('添加失败:\n$e')), 48 | ); 49 | } 50 | }, 51 | ), 52 | ), 53 | Expanded( 54 | child: Observer( 55 | builder: (context) => ListView( 56 | children: adapterSearchController.jsAdapters 57 | .map((adapter) => ListTile( 58 | title: Text(adapter.name), 59 | subtitle: Text('适配器地址:${adapter.sourceUrl}'), 60 | trailing: IconButton( 61 | icon: const Icon(Icons.close), 62 | onPressed: () { 63 | adapterSearchController.removeJsAdapter(adapter); 64 | }, 65 | ), 66 | )) 67 | .toList(), 68 | ), 69 | ), 70 | ), 71 | Container( 72 | padding: const EdgeInsets.all(16.0), 73 | child: Row( 74 | children: [ 75 | const Expanded( 76 | child: Text( 77 | overflow: TextOverflow.clip, 78 | maxLines: null, 79 | '新的视频源可以通过添加自定义JavaScript适配器来添加,而不用依赖于作者更新。关于添加自定义Javascript适配器的更多信息,请在github查看。', 80 | ), 81 | ), 82 | IconButton( 83 | onPressed: () => launchUrlString( 84 | 'https://github.com/KNKPA/KNKPAnime-js-adapters'), 85 | icon: const Icon(Icons.open_in_new)) 86 | ], 87 | ), 88 | ), 89 | ], 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/pages/settings/settings_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_modular/flutter_modular.dart'; 4 | import 'package:knkpanime/utils/utils.dart'; 5 | import 'package:logger/logger.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:window_manager/window_manager.dart'; 8 | 9 | class SettingsController { 10 | static late final SharedPreferences prefs; 11 | late final logger = Modular.get(); 12 | 13 | double get fontSize => 14 | prefs.getDouble('fontSize') ?? (Utils.isDesktop() ? 25 : 16); 15 | double get danmakuArea => prefs.getDouble('danmakuArea') ?? 1; 16 | double get danmakuOpacity => prefs.getDouble('danmakuOpacity') ?? 1; 17 | bool get hideScrollDanmakus => prefs.getBool('hideScrollDanmakus') ?? false; 18 | bool get hideTopDanmakus => prefs.getBool('hideTopDanmakus') ?? false; 19 | bool get hideBottomDanmakus => prefs.getBool('hideBottomDanmakus') ?? false; 20 | bool get alwaysOnTop => prefs.getBool('alwaysOnTop') ?? false; 21 | bool get disableGithubProxy => prefs.getBool('disableGithubProxy') ?? false; 22 | bool get useWebViewAdapters => prefs.getBool('useWebViewAdapters') ?? true; 23 | List get jsAdapters => prefs.getStringList('jsAdapters') ?? []; 24 | bool get danmakuEnabled => prefs.getBool('danmakuEnabled') ?? true; 25 | bool get darkModeEnabled => prefs.getBool('darkModeEnabled') ?? false; 26 | bool get useDefaultFont => prefs.getBool('useDefaultFont') ?? true; 27 | String get themeMode => prefs.getString('themeMode') ?? '跟随系统'; 28 | String? get customImageSet => prefs.getString('customImageSet'); 29 | 30 | set customImageSet(String? value) { 31 | if (value == null) { 32 | prefs.remove('customImageSet'); 33 | logger.i('Image set is set to default'); 34 | return; 35 | } 36 | prefs.setString('customImageSet', value!); 37 | logger.i('Image set is set to $value'); 38 | } 39 | 40 | set themeMode(String value) { 41 | prefs.setString('themeMode', value); 42 | logger.i('System theme mode set to $value'); 43 | } 44 | 45 | set useDefaultFont(bool value) { 46 | prefs.setBool('useDefaultFont', value); 47 | logger.i('Use default font set to $value'); 48 | } 49 | 50 | set darkModeEnabled(bool value) { 51 | prefs.setBool('darkModeEnabled', value); 52 | logger.i('Dark mode set to $value'); 53 | } 54 | 55 | set fontSize(double size) { 56 | prefs.setDouble('fontSize', size); 57 | logger.i('Font size set to $size'); 58 | } 59 | 60 | set danmakuArea(double value) { 61 | prefs.setDouble('danmakuArea', value); 62 | logger.i('Danmaku area set to $value'); 63 | } 64 | 65 | set danmakuOpacity(double value) { 66 | prefs.setDouble('danmakuOpacity', value); 67 | logger.i('Danmaku opacity set to $value'); 68 | } 69 | 70 | set hideScrollDanmakus(bool value) { 71 | prefs.setBool('hideScrollDanmakus', value); 72 | logger.i('Hide scroll danmakus set to $value'); 73 | } 74 | 75 | set hideTopDanmakus(bool value) { 76 | prefs.setBool('hideTopDanmakus', value); 77 | logger.i('Hide top danmakus set to $value'); 78 | } 79 | 80 | set hideBottomDanmakus(bool value) { 81 | prefs.setBool('hideBottomDanmakus', value); 82 | logger.i('Hide bottom danmakus set to $value'); 83 | } 84 | 85 | set alwaysOnTop(bool value) { 86 | prefs.setBool('alwaysOnTop', value); 87 | windowManager.setAlwaysOnTop(value); 88 | logger.i('Always on top set to $value'); 89 | } 90 | 91 | set disableGithubProxy(bool value) { 92 | prefs.setBool('disableGithubProxy', value); 93 | logger.i('disableGithubProxy set to $value'); 94 | } 95 | 96 | set useWebViewAdapters(bool value) { 97 | prefs.setBool('useWebViewAdapters', value); 98 | logger.i('useWebViewAdapters set to $value'); 99 | } 100 | 101 | set jsAdapters(List value) { 102 | prefs.setStringList('jsAdapters', value); 103 | } 104 | 105 | set danmakuEnabled(bool value) { 106 | prefs.setBool('danmakuEnabled', value); 107 | } 108 | 109 | static init() async { 110 | final p = await SharedPreferences.getInstance(); 111 | prefs = p; 112 | prefs.remove('showNewChanges'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/pages/settings/settings_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_modular/flutter_modular.dart'; 2 | import 'package:knkpanime/pages/settings/image_set_config_page.dart'; 3 | import 'package:knkpanime/pages/settings/js_adapter_config_page.dart'; 4 | import 'package:knkpanime/pages/settings/settings_page.dart'; 5 | 6 | class SettingsModule extends Module { 7 | @override 8 | void routes(RouteManager r) { 9 | r.child('/', child: (context) => SettingsPage()); 10 | r.child('/jsAdapterConfig', child: (context) => JsAdapterConfigPage()); 11 | r.child('/imageSetConfig', child: (context) => ImageSetConfigPage()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/bangumi.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:knkpanime/models/anime_info.dart'; 3 | 4 | class Bangumi { 5 | Bangumi._(); 6 | 7 | static const _todayBangumiApi = '/calendar'; 8 | static const _searchApi = '/search/subject/'; 9 | static final _dio = Dio(BaseOptions(baseUrl: 'https://api.bgm.tv', headers: { 10 | 'User-Agent': 'KNKPA/KNKPAnime', 11 | })); 12 | 13 | static Future>> fetchTodayAnime() async { 14 | var ret = >[]; 15 | var resp = await _dio.get(_todayBangumiApi); 16 | var temp = []; 17 | for (var weekday in resp.data) { 18 | for (var anime in weekday['items']) { 19 | temp.add(_toAnimeInfo(anime)); 20 | } 21 | ret.add(temp); 22 | temp = []; 23 | } 24 | return ret; 25 | } 26 | 27 | static Future<(List, int)> search(String keyword, 28 | {int? start, int? maxResults}) async { 29 | var ret = []; 30 | var resp = await _dio.get(_searchApi + keyword, queryParameters: { 31 | 'type': 2, 32 | 'max_results': maxResults ?? 25, 33 | 'start': start ?? 0, 34 | 'responseGroup': 'large', 35 | }); 36 | if (resp.statusCode != 200 || resp.data is String) { 37 | throw resp.toString(); 38 | } 39 | for (var anime in resp.data['list']) { 40 | ret.add(_toAnimeInfo(anime)); 41 | } 42 | return (ret, resp.data['results'] as int); 43 | } 44 | 45 | static AnimeInfo _toAnimeInfo(dynamic anime) { 46 | var images = anime['images'] == null 47 | ? null 48 | : Map.from(anime['images']); 49 | return AnimeInfo(anime['id'], anime['url'], anime['name_cn'], anime['name'], 50 | anime['summary'], anime['air_date'], images); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils/danmaku.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:ffi'; 3 | import 'dart:io'; 4 | import 'package:crypto/crypto.dart'; 5 | 6 | import 'package:dio/dio.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:knkpanime/models/danmaku.dart'; 9 | import 'package:knkpanime/utils/utils.dart'; 10 | import 'package:media_kit/ffi/src/utf8.dart'; 11 | import 'package:ns_danmaku/ns_danmaku.dart'; 12 | 13 | import '../models/danmaku.dart'; 14 | 15 | class DanmakuRequest { 16 | static var _cachedAnimeId = 0; 17 | static var _cachedEpisodes = []; 18 | static const _searchAnimeApi = '/api/v2/search/anime'; 19 | static const _searchEpisodeApi = '/api/v2/bangumi/'; 20 | static const _getDanmakuApi = '/api/v2/comment/'; 21 | static const String _secret = String.fromEnvironment('APP_SECRET'); 22 | static const String _appId = String.fromEnvironment('APP_ID'); 23 | 24 | static Future> getMatchingAnimes( 25 | String animeName) async { 26 | try { 27 | var resp = await fetch(_searchAnimeApi, queryParameters: { 28 | 'keyword': animeName, 29 | }); 30 | List ret = []; 31 | resp.data['animes'].forEach((e) => ret.add( 32 | DanmakuAnimeInfo(e['animeId'], e['animeTitle'], e['episodeCount']))); 33 | return ret; 34 | } catch (e) { 35 | rethrow; 36 | } 37 | } 38 | 39 | static Future> getDanmakus(int animeId, int episode) async { 40 | // Maybe considering omit request episode infomation? 41 | // The episodeId seems to consist of two parts: 42 | // Anime id and episode index (starting from 1, pad left to 4 digits) 43 | // E.g., first episode's id of K-ON (animeId=6257) is 62570001. 44 | int episodeId; 45 | /* 46 | if (_cachedAnimeId != animeId) { 47 | try { 48 | var resp = await dio.get(_searchEpisodeApi + animeId.toString()); 49 | _cachedAnimeId = animeId; 50 | _cachedEpisodes = resp.data["bangumi"]["episodes"] 51 | .map((e) => e['episodeId'] as int) 52 | .toList(); 53 | } catch (e) { 54 | rethrow; 55 | } 56 | } 57 | */ 58 | episodeId = int.parse( 59 | animeId.toString() + (episode + 1).toString().padLeft(4, '0')); 60 | 61 | try { 62 | var resp = 63 | await fetch(_getDanmakuApi + episodeId.toString(), queryParameters: { 64 | 'withRelated': true, 65 | }); 66 | var ret = []; 67 | resp.data['comments'].forEach((e) { 68 | final info = e['p'].split(','); 69 | final offset = double.parse(info[0]); 70 | final pos = info[1] == '1' 71 | ? DanmakuItemType.scroll 72 | : (info[1] == '4' ? DanmakuItemType.bottom : DanmakuItemType.top); 73 | final color = Color(int.parse(info[2]) | 0xFF000000); 74 | 75 | ret.add(Danmaku(offset, e['m'], pos, color)); 76 | }); 77 | //return Utils.cast>(resp.data['comments'] 78 | // .map((e) => Danmaku(double.parse(e['p'].split(',')[0]), e['m'])) 79 | // .toList())!; 80 | return ret; 81 | } catch (e) { 82 | rethrow; 83 | } 84 | } 85 | 86 | static Future fetch(String path, 87 | {Map? queryParameters}) { 88 | final dio = Dio(BaseOptions( 89 | headers: { 90 | "User-Agent": 'KNKPA/KNKPAnime', 91 | "X-AppId": _appId, 92 | "X-Timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, 93 | }, 94 | baseUrl: 'https://api.dandanplay.net', 95 | )); 96 | final temp = _appId + 97 | (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString() + 98 | path + 99 | _secret; 100 | dio.options.headers['X-Signature'] = 101 | base64.encode(sha256.convert(utf8.encode(temp)).bytes); 102 | return dio.get(path, queryParameters: queryParameters); 103 | } 104 | 105 | DanmakuRequest._(); 106 | } 107 | -------------------------------------------------------------------------------- /lib/utils/storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:hive/hive.dart'; 4 | import 'package:knkpanime/models/anime_info.dart'; 5 | import 'package:knkpanime/models/episode.dart'; 6 | import 'package:knkpanime/models/history.dart'; 7 | import 'package:knkpanime/models/image_set.dart'; 8 | import 'package:knkpanime/models/series.dart'; 9 | import 'package:knkpanime/pages/settings/settings_controller.dart'; 10 | 11 | class Storage { 12 | static late Box histories; 13 | static late Box favorites; 14 | static ImageSet get imageSet { 15 | if (SettingsController().customImageSet == null) { 16 | return _defaultImageSet; 17 | } 18 | final imageSet = imageSets.get(SettingsController().customImageSet!); 19 | return imageSet ?? _defaultImageSet; 20 | } 21 | 22 | static late Box imageSets; 23 | static late ImageSet _defaultImageSet; 24 | 25 | static Future init() async { 26 | Hive.registerAdapter(HistoryAdapter()); 27 | Hive.registerAdapter(ProgressAdapter()); 28 | Hive.registerAdapter(EpisodeAdapter()); 29 | Hive.registerAdapter(SeriesAdapter()); 30 | Hive.registerAdapter(AnimeInfoAdapter()); 31 | Hive.registerAdapter(ImageSetAdapter()); 32 | histories = await Hive.openBox('histories'); 33 | favorites = await Hive.openBox('favorites'); 34 | imageSets = await Hive.openBox('imageSets'); 35 | _defaultImageSet = ImageSet( 36 | (await rootBundle.load('assets/images/sidemenu_background.jpg')) 37 | .buffer 38 | .asUint8List(), 39 | (await rootBundle.load('assets/images/placeholder.jpg')) 40 | .buffer 41 | .asUint8List(), 42 | (await rootBundle.load('assets/images/no_image.jpg')) 43 | .buffer 44 | .asUint8List()); 45 | } 46 | 47 | Storage._(); 48 | } 49 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:knkpanime/adapters/adapter_base.dart'; 5 | 6 | class Utils { 7 | Utils._(); 8 | 9 | static bool isDesktop() => 10 | Platform.isMacOS || Platform.isWindows || Platform.isLinux; 11 | 12 | static bool isSmallScreen(BuildContext context) => 13 | MediaQuery.of(context).size.width < 850; 14 | 15 | static String dur2str(Duration duration) { 16 | return '${duration.inHours.toString().padLeft(2, '0')}:${duration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(duration.inSeconds.remainder(60)).toString().padLeft(2, '0')}'; 17 | } 18 | 19 | static Color getColorFromStatus(SearchStatus status) { 20 | switch (status) { 21 | case SearchStatus.success: 22 | return Colors.green; 23 | case SearchStatus.failed: 24 | return Colors.red; 25 | case SearchStatus.pending: 26 | return Colors.grey; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/widgets/anime_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/models/anime_info.dart'; 4 | import 'package:knkpanime/pages/favorite/favorite_controller.dart'; 5 | import 'package:cached_network_image/cached_network_image.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:knkpanime/utils/storage.dart'; 8 | 9 | class AnimeCard extends StatefulWidget { 10 | final AnimeInfo anime; 11 | final Function(AnimeInfo) onTap; 12 | 13 | const AnimeCard(this.anime, this.onTap, {super.key}); 14 | 15 | @override 16 | State createState() => _AnimeCardState(); 17 | } 18 | 19 | class _AnimeCardState extends State { 20 | late var favoriteController = Modular.get(); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return InkWell( 25 | onTap: () => widget.onTap(widget.anime), 26 | child: Card( 27 | child: Row( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | ClipRRect( 31 | borderRadius: BorderRadius.circular(8.0), 32 | child: CachedNetworkImage( 33 | placeholder: (context, url) => Image.memory( 34 | width: 100.0, 35 | height: 150.0, 36 | fit: BoxFit.cover, 37 | Storage.imageSet.coverPlaceholder, 38 | ), 39 | imageUrl: widget.anime.images?['large'] ?? '', 40 | width: 100.0, 41 | height: 150.0, 42 | fit: BoxFit.cover, 43 | fadeOutDuration: const Duration(milliseconds: 120), 44 | fadeInDuration: const Duration(milliseconds: 120), 45 | // filterQuality: FilterQuality.low, 46 | errorWidget: (context, error, stackTrace) { 47 | Modular.get().w(error); 48 | return Image.memory( 49 | width: 100.0, 50 | height: 150.0, 51 | fit: BoxFit.cover, 52 | Storage.imageSet.coverNoImage, 53 | ); 54 | }, 55 | ), 56 | ), 57 | const SizedBox(width: 10.0), 58 | Expanded( 59 | child: Column( 60 | crossAxisAlignment: CrossAxisAlignment.stretch, 61 | children: [ 62 | Text( 63 | widget.anime.name, 64 | style: const TextStyle( 65 | fontSize: 16.0, fontWeight: FontWeight.bold), 66 | ), 67 | const SizedBox(height: 5.0), 68 | Text( 69 | widget.anime.summary, 70 | maxLines: 3, 71 | overflow: TextOverflow.ellipsis, 72 | ), 73 | ], 74 | ), 75 | ), 76 | IconButton( 77 | icon: Icon( 78 | favoriteController.isFavorite(widget.anime) 79 | ? Icons.favorite 80 | : Icons.favorite_border, 81 | color: Colors.red, 82 | ), 83 | onPressed: () { 84 | favoriteController.isFavorite(widget.anime) 85 | ? favoriteController.deleteFavorite(widget.anime) 86 | : favoriteController.addFavorite(widget.anime); 87 | setState(() {}); 88 | }, 89 | ) 90 | ], 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/widgets/series_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | import 'package:knkpanime/models/history.dart'; 4 | import 'package:knkpanime/models/series.dart'; 5 | import 'package:knkpanime/utils/utils.dart'; 6 | import 'package:cached_network_image/cached_network_image.dart'; 7 | import 'package:logger/logger.dart'; 8 | 9 | import '../utils/storage.dart'; 10 | 11 | class SeriesCard extends StatelessWidget { 12 | final Series series; 13 | final Progress? progress; 14 | final Function(Series) onTap; 15 | final String sourceName; 16 | final Function()? onDelete; 17 | 18 | const SeriesCard(this.series, this.progress, this.onTap, this.sourceName, 19 | {super.key, this.onDelete}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return InkWell( 24 | onTap: () => onTap(series), 25 | child: Card( 26 | child: Row( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | ClipRRect( 30 | borderRadius: BorderRadius.circular(8.0), 31 | child: CachedNetworkImage( 32 | placeholder: (context, url) => Image.memory( 33 | width: 100.0, 34 | height: 150.0, 35 | fit: BoxFit.cover, 36 | Storage.imageSet.coverPlaceholder, 37 | ), 38 | imageUrl: series.image ?? '', 39 | width: 100.0, 40 | height: 150.0, 41 | fit: BoxFit.cover, 42 | fadeOutDuration: const Duration(milliseconds: 120), 43 | fadeInDuration: const Duration(milliseconds: 120), 44 | // filterQuality: FilterQuality.low, 45 | errorWidget: (context, error, stackTrace) { 46 | Modular.get().w(error); 47 | return Image.memory( 48 | width: 100.0, 49 | height: 150.0, 50 | fit: BoxFit.cover, 51 | Storage.imageSet.coverNoImage, 52 | ); 53 | }, 54 | ), 55 | ), 56 | const SizedBox(width: 10.0), 57 | Expanded( 58 | child: Column( 59 | crossAxisAlignment: CrossAxisAlignment.stretch, 60 | children: [ 61 | Text( 62 | series.name, 63 | style: const TextStyle( 64 | fontSize: 16.0, fontWeight: FontWeight.bold), 65 | ), 66 | Text( 67 | '番剧源:$sourceName', 68 | style: const TextStyle(fontSize: 12), 69 | ), 70 | progress != null 71 | ? Text( 72 | '上次看到 ${progress!.episode.name} ${Utils.dur2str(progress!.progress)}', 73 | style: const TextStyle(fontSize: 12), 74 | ) 75 | : Container(), 76 | const SizedBox(height: 5.0), 77 | Text( 78 | series.description ?? '', 79 | maxLines: 3, 80 | overflow: TextOverflow.ellipsis, 81 | ), 82 | ], 83 | ), 84 | ), 85 | onDelete == null 86 | ? Container() 87 | : IconButton( 88 | icon: const Icon(Icons.delete), 89 | onPressed: onDelete!, 90 | ), 91 | ], 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | void fl_register_plugins(FlPluginRegistry* registry) { 18 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 20 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 21 | g_autoptr(FlPluginRegistrar) flutter_js_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterJsPlugin"); 23 | flutter_js_plugin_register_with_registrar(flutter_js_registrar); 24 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 26 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 27 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 28 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 29 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 30 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 31 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 32 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 33 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 34 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 35 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 36 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 37 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 38 | window_manager_plugin_register_with_registrar(window_manager_registrar); 39 | } 40 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_linux 7 | flutter_js 8 | media_kit_libs_linux 9 | media_kit_video 10 | screen_retriever 11 | url_launcher_linux 12 | window_manager 13 | ) 14 | 15 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 16 | media_kit_native_event_loop 17 | ) 18 | 19 | set(PLUGIN_BUNDLED_LIBRARIES) 20 | 21 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 26 | endforeach(plugin) 27 | 28 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_selector_macos 9 | import flutter_inappwebview_macos 10 | import flutter_js 11 | import media_kit_libs_macos_video 12 | import media_kit_video 13 | import package_info_plus 14 | import path_provider_foundation 15 | import screen_brightness_macos 16 | import screen_retriever 17 | import shared_preferences_foundation 18 | import sqflite 19 | import url_launcher_macos 20 | import wakelock_plus 21 | import window_manager 22 | 23 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 24 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 25 | InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) 26 | FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin")) 27 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 28 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 29 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 30 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 31 | ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) 32 | ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) 33 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 34 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 35 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 36 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 37 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 38 | } 39 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | }, 6 | "images": [ 7 | { 8 | "size": "16x16", 9 | "idiom": "mac", 10 | "filename": "app_icon_16.png", 11 | "scale": "1x" 12 | }, 13 | { 14 | "size": "16x16", 15 | "idiom": "mac", 16 | "filename": "app_icon_32.png", 17 | "scale": "2x" 18 | }, 19 | { 20 | "size": "32x32", 21 | "idiom": "mac", 22 | "filename": "app_icon_32.png", 23 | "scale": "1x" 24 | }, 25 | { 26 | "size": "32x32", 27 | "idiom": "mac", 28 | "filename": "app_icon_64.png", 29 | "scale": "2x" 30 | }, 31 | { 32 | "size": "128x128", 33 | "idiom": "mac", 34 | "filename": "app_icon_128.png", 35 | "scale": "1x" 36 | }, 37 | { 38 | "size": "128x128", 39 | "idiom": "mac", 40 | "filename": "app_icon_256.png", 41 | "scale": "2x" 42 | }, 43 | { 44 | "size": "256x256", 45 | "idiom": "mac", 46 | "filename": "app_icon_256.png", 47 | "scale": "1x" 48 | }, 49 | { 50 | "size": "256x256", 51 | "idiom": "mac", 52 | "filename": "app_icon_512.png", 53 | "scale": "2x" 54 | }, 55 | { 56 | "size": "512x512", 57 | "idiom": "mac", 58 | "filename": "app_icon_512.png", 59 | "scale": "1x" 60 | }, 61 | { 62 | "size": "512x512", 63 | "idiom": "mac", 64 | "filename": "app_icon_1024.png", 65 | "scale": "2x" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = knkpanime 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.knkpanime 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | knkpanime 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knkpanime", 3 | "short_name": "knkpanime", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(knkpanime LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "knkpanime") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /windows/create_shortcut.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set SCRIPT="%TEMP%\%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.vbs" 4 | 5 | echo Set oWS = WScript.CreateObject("WScript.Shell") >> %SCRIPT% 6 | echo sLinkFile = "%CD%\build\windows\x64\runner\knkpanime.lnk" >> %SCRIPT% 7 | echo Set oLink = oWS.CreateShortcut(sLinkFile) >> %SCRIPT% 8 | echo oLink.TargetPath = "%CD%\build\windows\x64\runner\Release\knkpanime.exe" >> %SCRIPT% 9 | echo oLink.Save >> %SCRIPT% 10 | 11 | cscript /nologo %SCRIPT% 12 | del %SCRIPT% -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | void RegisterPlugins(flutter::PluginRegistry* registry) { 20 | FileSelectorWindowsRegisterWithRegistrar( 21 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 22 | FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( 23 | registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); 24 | FlutterJsPluginRegisterWithRegistrar( 25 | registry->GetRegistrarForPlugin("FlutterJsPlugin")); 26 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 27 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 28 | MediaKitVideoPluginCApiRegisterWithRegistrar( 29 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 30 | ScreenBrightnessWindowsPluginRegisterWithRegistrar( 31 | registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); 32 | ScreenRetrieverPluginRegisterWithRegistrar( 33 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 34 | UrlLauncherWindowsRegisterWithRegistrar( 35 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 36 | WindowManagerPluginRegisterWithRegistrar( 37 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 38 | } 39 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_windows 7 | flutter_inappwebview_windows 8 | flutter_js 9 | media_kit_libs_windows_video 10 | media_kit_video 11 | screen_brightness_windows 12 | screen_retriever 13 | url_launcher_windows 14 | window_manager 15 | ) 16 | 17 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 18 | media_kit_native_event_loop 19 | ) 20 | 21 | set(PLUGIN_BUNDLED_LIBRARIES) 22 | 23 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 25 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 28 | endforeach(plugin) 29 | 30 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 31 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 32 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 33 | endforeach(ffi_plugin) 34 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "knkpanime" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "knkpanime" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "knkpanime.exe" "\0" 98 | VALUE "ProductName", "knkpanime" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"knkpanime", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNKPA/KNKPAnime/bf0032a159938cfd3307d2692d6f42073734ca1f/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | --------------------------------------------------------------------------------