├── .github └── workflows │ ├── Package.yml │ └── Release.yml ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── opensource │ │ │ │ └── kobi │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_background.png │ │ │ ├── ic_foreground.png │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_background.png │ │ │ ├── ic_foreground.png │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_background.png │ │ │ ├── ic_foreground.png │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_background.png │ │ │ ├── ic_foreground.png │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_background.png │ │ │ ├── ic_foreground.png │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ci ├── Cargo.lock ├── Cargo.toml ├── src │ ├── check_asset │ │ └── main.rs │ ├── check_release │ │ └── main.rs │ ├── common.rs │ ├── lib.rs │ └── upload_asset │ │ └── main.rs ├── version.code.txt └── version.info.txt ├── flutter_rust_bridge.yaml ├── images ├── G01.png └── G02.png ├── integration_test └── simple_test.dart ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── 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 ├── assets │ ├── .keep │ ├── 0.png │ ├── icon.png │ ├── startup.png │ └── version.txt ├── commons.dart ├── configs │ ├── api_host.dart │ ├── app_orientation.dart │ ├── app_theme.dart │ ├── cache_time.dart │ ├── chapter_order_newest.dart │ ├── collect_ordering.dart │ ├── comic_grid_columns.dart │ ├── comic_pager_type.dart │ ├── configs.dart │ ├── list_volume.dart │ ├── login.dart │ ├── map_config.dart │ ├── no_pager_animation.dart │ ├── proxy.dart │ ├── reader_controller_type.dart │ ├── reader_direction.dart │ ├── reader_slider_position.dart │ ├── reader_type.dart │ └── versions.dart ├── cross.dart ├── main.dart ├── screens │ ├── about_screen.dart │ ├── app_screen.dart │ ├── collected_comics_account_screen.dart │ ├── comic_download_screen.dart │ ├── comic_info_screen.dart │ ├── comic_reader_screen.dart │ ├── comic_search_screen.dart │ ├── components │ │ ├── android_version.dart │ │ ├── badged.dart │ │ ├── comic_card.dart │ │ ├── comic_list.dart │ │ ├── comic_pager.dart │ │ ├── comment_list.dart │ │ ├── commnet_card.dart │ │ ├── commons.dart │ │ ├── content_error.dart │ │ ├── content_loading.dart │ │ ├── download_comic_card.dart │ │ ├── error_types.dart │ │ ├── expandable_text.dart │ │ ├── fade_image_widget.dart │ │ ├── file_photo_view_screen.dart │ │ ├── flutter_search_bar_base.dart │ │ ├── image_cache_provider.dart │ │ ├── images.dart │ │ └── router.dart │ ├── discovery_screen.dart │ ├── download_comic_info_screen.dart │ ├── downloads_screen.dart │ ├── histories_screen.dart │ ├── init_screen.dart │ ├── login_screen.dart │ ├── rank_screen.dart │ ├── recommends_screen.dart │ ├── settings_screen.dart │ └── user_screen.dart └── src │ └── rust │ ├── api │ ├── api.dart │ └── simple.dart │ ├── copy_client │ └── dtos.dart │ ├── frb_generated.dart │ ├── frb_generated.io.dart │ ├── frb_generated.web.dart │ └── udto.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── appimage │ ├── AppRun.desktop │ └── AppRun.png ├── 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 ├── rust ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── api │ ├── api.rs │ ├── mod.rs │ └── simple.rs │ ├── copy_client │ ├── client.rs │ ├── dtos.rs │ ├── lib.rs │ ├── mod.rs │ ├── tests.rs │ └── types.rs │ ├── database │ ├── active │ │ ├── comic_view_log.rs │ │ ├── local_collect.rs │ │ └── mod.rs │ ├── cache │ │ ├── image_cache.rs │ │ ├── mod.rs │ │ └── web_cache.rs │ ├── download │ │ ├── download_comic.rs │ │ ├── download_comic_chapter.rs │ │ ├── download_comic_group.rs │ │ ├── download_comic_page.rs │ │ └── mod.rs │ ├── mod.rs │ └── properties │ │ ├── mod.rs │ │ └── property.rs │ ├── downloading.rs │ ├── exports.rs │ ├── frb_generated.rs │ ├── lib.rs │ ├── udto.rs │ └── utils.rs ├── rust_builder ├── .gitignore ├── README.md ├── android │ ├── .gitignore │ ├── build.gradle │ ├── settings.gradle │ └── src │ │ └── main │ │ └── AndroidManifest.xml ├── cargokit │ ├── .gitignore │ ├── LICENSE │ ├── README │ ├── build_pod.sh │ ├── build_tool │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── bin │ │ │ └── build_tool.dart │ │ ├── lib │ │ │ ├── build_tool.dart │ │ │ └── src │ │ │ │ ├── android_environment.dart │ │ │ │ ├── artifacts_provider.dart │ │ │ │ ├── build_cmake.dart │ │ │ │ ├── build_gradle.dart │ │ │ │ ├── build_pod.dart │ │ │ │ ├── build_tool.dart │ │ │ │ ├── builder.dart │ │ │ │ ├── cargo.dart │ │ │ │ ├── crate_hash.dart │ │ │ │ ├── environment.dart │ │ │ │ ├── logging.dart │ │ │ │ ├── options.dart │ │ │ │ ├── precompile_binaries.dart │ │ │ │ ├── rustup.dart │ │ │ │ ├── target.dart │ │ │ │ ├── util.dart │ │ │ │ └── verify_binaries.dart │ │ ├── pubspec.lock │ │ └── pubspec.yaml │ ├── cmake │ │ ├── cargokit.cmake │ │ └── resolve_symlinks.ps1 │ ├── gradle │ │ └── plugin.gradle │ ├── run_build_tool.cmd │ └── run_build_tool.sh ├── ios │ ├── Classes │ │ └── dummy_file.c │ └── rust_lib_kobi.podspec ├── linux │ └── CMakeLists.txt ├── macos │ ├── Classes │ │ └── dummy_file.c │ └── rust_lib_kobi.podspec ├── pubspec.yaml └── windows │ ├── .gitignore │ └── CMakeLists.txt ├── scripts └── thin-payload.sh ├── test_driver └── integration_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 ├── 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 /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | target/ 48 | android/app/.cxx/ 49 | -------------------------------------------------------------------------------- /.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: "4cf269e36de2573851eaef3c763994f8f9be494d" 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: 4cf269e36de2573851eaef3c763994f8f9be494d 17 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 18 | - platform: android 19 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 20 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 21 | - platform: ios 22 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 23 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 24 | - platform: linux 25 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 26 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 27 | - platform: macos 28 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 29 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 30 | - platform: web 31 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 32 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 33 | - platform: windows 34 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 35 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Flutter", 6 | "type": "dart", 7 | "request": "launch", 8 | "program": "lib/main.dart" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPaths": [ 3 | "/Users/niuhuan/Runtimes/flutter-3.13.9", 4 | "/Users/niuhuan/Runtimes/flutter-3.10.5", 5 | "/Users/niuhuan/Runtimes/flutter-3.7.3", 6 | "/Users/niuhuan/Runtimes/flutter-3.22.1", 7 | "/Users/niuhuan/Runtimes/flutter-3.29.3" 8 | ], 9 | "dart.addSdkToTerminalP/ath": true, 10 | "dart.flutterSdkPath": "/Users/niuhuan/Runtimes/flutter-3.29.3" 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | Kobi Comic 6 | 7 | Trendshift 8 | 9 | [![license](https://img.shields.io/github/license/niuhuan/kobi)](https://raw.githubusercontent.com/niuhuan/kobi/master/LICENSE) 10 | [![releases](https://img.shields.io/github/v/release/niuhuan/kobi)](https://github.com/niuhuan/kobi/releases) 11 | [![downloads](https://img.shields.io/github/downloads/niuhuan/kobi/total)](https://github.com/niuhuan/kobi/releases) 12 | 13 |

14 |
15 | 16 |
17 | 18 | 19 | 20 | 一个简洁大方的漫画客户端, 同时支持 Android / iOS / MacOS / Windows / Linux. 21 | 22 | 此APP含有"吸烟/饮酒/斗殴/言情/两性"等内容或间接性描述,请在使用过程中遵守当地法律法规。 23 | 24 | 如果您觉得此软件对您有帮助,可以star进行支持。同时欢迎您issue,一起让软件变得更好。 25 | 26 | 仓库地址 https://github.com/niuhuan/kobi 27 | 28 | ## 预览 29 | 30 | ![G01](images/G01.png) 31 | ![G02](images/G02.png) 32 | 33 | ## 其他 34 | 35 | ### 数据保存位置 36 | 37 | - macos: `~/Library/Application Support/opensource/kobi` 38 | - windows: `%CURRENT_DIR%\data` 39 | - linux: `$HOME/.opensource/kobi` 40 | 41 | ### 本地构建 42 | 43 | 参考 Github Actions 的构建脚本 44 | 45 | ## 技术架构 46 | 47 | 客户端使用前后端分离架构, flutter作为渲染框架. rust作为底层调度网络和文件系统. Flutter与rust均为跨平台编程语言, 以此支持 android/iOS/windows/macOS 等不同操作系统. 48 | 49 | ![](https://raw.githubusercontent.com/fzyzcjy/flutter_rust_bridge/master/book/logo.png) 50 | -------------------------------------------------------------------------------- /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 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /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/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "opensource.kobi" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = '29.0.13113456' // flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "opensource.kobi" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /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 | 13 | 14 | 15 | 16 | 17 | 22 | 31 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 49 | 50 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /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-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-hdpi/ic_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-hdpi/ic_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-mdpi/ic_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-mdpi/ic_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xhdpi/ic_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xxhdpi/ic_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /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 -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.suppressUnsupportedCompileSdk=34,35 5 | -------------------------------------------------------------------------------- /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-8.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 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.1.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /ci/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ci" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "check-release" 8 | path = "src/check_release/main.rs" 9 | 10 | [[bin]] 11 | name = "check-asset" 12 | path = "src/check_asset/main.rs" 13 | 14 | [[bin]] 15 | name = "upload-asset" 16 | path = "src/upload_asset/main.rs" 17 | 18 | [dependencies] 19 | anyhow = "1.0.86" 20 | reqwest = { version = "0.12.4", features = ["json"] } 21 | serde = "1.0.203" 22 | serde_derive = "1.0.203" 23 | serde_json = "1.0.117" 24 | tokio = { version = "1.38.0", features = ["full"] } 25 | -------------------------------------------------------------------------------- /ci/src/check_release/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::collections::HashMap; 3 | use std::process::exit; 4 | 5 | const UA: &str = "actions ci"; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let gh_token = std::env::var("GITHUB_TOKEN")?; 10 | if gh_token.is_empty() { 11 | panic!("Please set GITHUB_TOKEN"); 12 | } 13 | 14 | let repo = std::env::var("GITHUB_REPOSITORY")?; 15 | if repo.is_empty() { 16 | panic!("Can't got repo path"); 17 | } 18 | 19 | let branch = std::env::var("GITHUB_HEAD_REF")?; 20 | if repo.is_empty() { 21 | panic!("Can't got repo branch"); 22 | } 23 | 24 | let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; 25 | let vs_info_txt = tokio::fs::read_to_string("version.info.txt").await?; 26 | 27 | let code = vs_code_txt.trim(); 28 | let info = vs_info_txt.trim(); 29 | 30 | let client = reqwest::ClientBuilder::new().user_agent(UA).build()?; 31 | 32 | let release_url = format!("https://api.github.com/repos/{repo}/releases/tags/{code}"); 33 | let check_response = client.get(release_url).send().await?; 34 | 35 | match check_response.status().as_u16() { 36 | 200 => { 37 | println!("release exists"); 38 | exit(0); 39 | } 40 | 404 => (), 41 | code => { 42 | let text = check_response.text().await?; 43 | panic!("error for check release : {} : {}", code, text); 44 | } 45 | } 46 | drop(check_response); 47 | 48 | // 404 49 | 50 | let releases_url = format!("https://api.github.com/repos/{repo}/releases"); 51 | let check_response = client 52 | .post(releases_url) 53 | .header("Authorization", format!("token {}", gh_token)) 54 | .json(&{ 55 | let mut params = HashMap::::new(); 56 | params.insert("tag_name".to_string(), code.to_string()); 57 | params.insert("target_commitish".to_string(), branch); 58 | params.insert("name".to_string(), code.to_string()); 59 | params.insert("body".to_string(), info.to_string()); 60 | params 61 | }) 62 | .send() 63 | .await?; 64 | 65 | match check_response.status().as_u16() { 66 | 201 => (), 67 | code => { 68 | let text = check_response.text().await?; 69 | panic!("error for create release : {} : {}", code, text); 70 | } 71 | } 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /ci/src/common.rs: -------------------------------------------------------------------------------- 1 | pub const UA: &str = "github actions"; 2 | 3 | pub fn asset_name(app_name: &str, code: &str, target: &str) -> String { 4 | match target { 5 | "macos" => format!("{app_name}-{code}.dmg"), 6 | "ios" => format!("{app_name}-{code}-nosign.ipa"), 7 | "windows" => format!("{app_name}-{code}-windows-x86_64.zip"), 8 | "linux" => format!("{app_name}-{code}-linux-x86_64.AppImage"), 9 | "android-arm32" => format!("{app_name}-{code}-arm32.apk"), 10 | "android-arm64" => format!("{app_name}-{code}-arm64.apk"), 11 | "android-x86_64" => format!("{app_name}-{code}-x86_64.apk"), 12 | un => panic!("unknown target : {un}"), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ci/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | -------------------------------------------------------------------------------- /ci/version.code.txt: -------------------------------------------------------------------------------- 1 | v0.0.19 2 | -------------------------------------------------------------------------------- /ci/version.info.txt: -------------------------------------------------------------------------------- 1 | v0.0.19 2 | - [x] ✨ 安卓使用音量键翻页 3 | - [x] ✨ 详情展示章节排序反转(设置项) 4 | - [x] ♻️ 默认浏览模式改成封面 5 | - [x] ♻️ 设置界面一些颜色的优化 6 | - [x] ♻️ 恢复访问 7 | - [x] 🐛 修复设置界面主题设置后文字不变 8 | -------------------------------------------------------------------------------- /flutter_rust_bridge.yaml: -------------------------------------------------------------------------------- 1 | rust_input: crate::api 2 | rust_root: rust/ 3 | dart_output: lib/src/rust -------------------------------------------------------------------------------- /images/G01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/images/G01.png -------------------------------------------------------------------------------- /images/G02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/images/G02.png -------------------------------------------------------------------------------- /integration_test/simple_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:kobi/main.dart'; 3 | import 'package:kobi/src/rust/frb_generated.dart'; 4 | import 'package:integration_test/integration_test.dart'; 5 | 6 | void main() { 7 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 8 | setUpAll(() async => await RustLib.init()); 9 | testWidgets('Can call rust function', (WidgetTester tester) async { 10 | await tester.pumpWidget(const MyApp()); 11 | expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /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.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | 11 | let controller = self.window.rootViewController as! FlutterViewController 12 | let channel = FlutterMethodChannel.init(name: "cross", binaryMessenger: controller as! FlutterBinaryMessenger) 13 | 14 | channel.setMethodCallHandler { (call, result) in 15 | Thread { 16 | if call.method == "root" { 17 | 18 | let documentsPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] 19 | 20 | result(documentsPath) 21 | 22 | } 23 | else if call.method == "saveImageToGallery"{ 24 | if let args = call.arguments as? String{ 25 | 26 | do { 27 | let fileURL: URL = URL(fileURLWithPath: args) 28 | let imageData = try Data(contentsOf: fileURL) 29 | 30 | if let uiImage = UIImage(data: imageData) { 31 | UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) 32 | result("OK") 33 | }else{ 34 | result(FlutterError(code: "", message: "Error loading image ", details: "")) 35 | } 36 | 37 | } catch { 38 | result(FlutterError(code: "", message: "Error loading image : \(error)", details: "")) 39 | } 40 | 41 | }else{ 42 | result(FlutterError(code: "", message: "params error", details: "")) 43 | } 44 | } 45 | else{ 46 | result(FlutterMethodNotImplemented) 47 | } 48 | }.start() 49 | } 50 | 51 | GeneratedPluginRegistrant.register(with: self) 52 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | kobi 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | kobi 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSApplicationCategoryType 28 | public.app-category.entertainment 29 | LSRequiresIPhoneOS 30 | 31 | NSPhotoLibraryAddUsageDescription 32 | Save images 33 | NSPhotoLibraryUsageDescription 34 | Usage images 35 | UIApplicationSupportsIndirectInputEvents 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | CADisableMinimumFrameDurationOnPhone 55 | 56 | UIApplicationSupportsIndirectInputEvents 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /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/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/lib/assets/.keep -------------------------------------------------------------------------------- /lib/assets/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/lib/assets/0.png -------------------------------------------------------------------------------- /lib/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/lib/assets/icon.png -------------------------------------------------------------------------------- /lib/assets/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/lib/assets/startup.png -------------------------------------------------------------------------------- /lib/assets/version.txt: -------------------------------------------------------------------------------- 1 | v0.0.5 -------------------------------------------------------------------------------- /lib/commons.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:convert'; 3 | 4 | import 'package:kobi/src/rust/copy_client/dtos.dart'; 5 | 6 | 7 | List stringAuthors(String data) { 8 | return mapAuthor(List.of(jsonDecode(data)).cast()); 9 | } 10 | 11 | List mapAuthor(List list) { 12 | List result = []; 13 | for (var value in list) { 14 | if (value['name'] != null && value['path_word'] != null) { 15 | result.add(Author( 16 | name: value['name'], 17 | pathWord: value['path_word'], 18 | )); 19 | } 20 | } 21 | return result; 22 | } 23 | 24 | ClassifyItem stringClassifyItem(String data) { 25 | return mapClassifyItem(jsonDecode(data)); 26 | } 27 | 28 | ClassifyItem mapClassifyItem(Map map) { 29 | return ClassifyItem( 30 | display: map['display'], 31 | value: map['value'], 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/configs/api_host.dart: -------------------------------------------------------------------------------- 1 | /// 代理设置 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../src/rust/api/api.dart' as api; 6 | import '../screens/components/commons.dart'; 7 | 8 | late String _currentApiHost; 9 | 10 | Future initApiHost() async { 11 | _currentApiHost = await api.getApiHost(); 12 | return null; 13 | } 14 | 15 | String currentApiHostName() { 16 | return _currentApiHost == "" ? "未设置" : _currentApiHost; 17 | } 18 | 19 | Future inputApiHost(BuildContext context) async { 20 | String? input = await displayTextInputDialog( 21 | context, 22 | src: _currentApiHost, 23 | title: '服务器', 24 | hint: '请输入服务器', 25 | desc: " ( 例如 https://domain.com ) ", 26 | ); 27 | if (input != null) { 28 | await api.setApiHost(api: input); 29 | _currentApiHost = input; 30 | } 31 | } 32 | 33 | Widget apiHostSetting() { 34 | return StatefulBuilder( 35 | builder: (BuildContext context, void Function(void Function()) setState) { 36 | return ListTile( 37 | title: const Text("服务器地址"), 38 | subtitle: Text(currentApiHostName()), 39 | onTap: () async { 40 | await inputApiHost(context); 41 | setState(() {}); 42 | }, 43 | ); 44 | }, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/configs/app_orientation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | import '../../src/rust/api/api.dart' as api; 7 | import '../../src/rust/udto.dart'; 8 | import '../screens/components/commons.dart'; 9 | 10 | enum AppOrientation { 11 | normal, 12 | landscape, 13 | portrait, 14 | } 15 | 16 | String appOrientationName(AppOrientation type, BuildContext context) { 17 | switch (type) { 18 | case AppOrientation.normal: 19 | return "正常"; 20 | case AppOrientation.landscape: 21 | return "横屏"; 22 | case AppOrientation.portrait: 23 | return "竖屏"; 24 | } 25 | } 26 | 27 | const _propertyName = "appOrientation"; 28 | late AppOrientation _appOrientation; 29 | 30 | Future initAppOrientation() async { 31 | _appOrientation = _fromString(await api.loadProperty(k: _propertyName)); 32 | _set(); 33 | } 34 | 35 | AppOrientation _fromString(String valueForm) { 36 | for (var value in AppOrientation.values) { 37 | if (value.toString() == valueForm) { 38 | return value; 39 | } 40 | } 41 | return AppOrientation.values.first; 42 | } 43 | 44 | AppOrientation get currentAppOrientation => _appOrientation; 45 | 46 | Future chooseAppOrientation(BuildContext context) async { 47 | final Map map = {}; 48 | for (var element in AppOrientation.values) { 49 | map[appOrientationName(element, context)] = element; 50 | } 51 | final newAppOrientation = await chooseMapDialog( 52 | context, 53 | title: "请选择APP方向", 54 | values: map, 55 | ); 56 | if (newAppOrientation != null) { 57 | await api.saveProperty(k: _propertyName, v: "$newAppOrientation"); 58 | _appOrientation = newAppOrientation; 59 | _set(); 60 | } 61 | } 62 | 63 | Widget appOrientationWidget() { 64 | if (!Platform.isAndroid && !Platform.isIOS) { 65 | return const SizedBox.shrink(); 66 | } 67 | return StatefulBuilder( 68 | builder: (BuildContext context, void Function(void Function()) setState) { 69 | return ListTile( 70 | title: const Text("APP方向"), 71 | subtitle: Text(appOrientationName(_appOrientation, context)), 72 | onTap: () async { 73 | await chooseAppOrientation(context); 74 | setState(() {}); 75 | }, 76 | ); 77 | }, 78 | ); 79 | } 80 | 81 | void _set() { 82 | if (Platform.isAndroid || Platform.isIOS) { 83 | switch (_appOrientation) { 84 | case AppOrientation.normal: 85 | SystemChrome.setPreferredOrientations([]); 86 | break; 87 | case AppOrientation.landscape: 88 | SystemChrome.setPreferredOrientations([ 89 | DeviceOrientation.landscapeLeft, 90 | DeviceOrientation.landscapeRight, 91 | ]); 92 | break; 93 | case AppOrientation.portrait: 94 | SystemChrome.setPreferredOrientations([ 95 | DeviceOrientation.portraitUp, 96 | DeviceOrientation.portraitDown, 97 | ]); 98 | break; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/configs/cache_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../src/rust/api/api.dart' as api; 4 | import '../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | const _propertyKey = "cache_time"; 8 | 9 | const _sec_of_day = 3600 * 24; 10 | const _src_of_3_day = _sec_of_day * 3; 11 | const _src_of_week = _sec_of_day * 7; 12 | 13 | const _sec_of_day_str = "1天"; 14 | const _src_of_3_day_str = "3天"; 15 | const _src_of_week_str = "1周"; 16 | 17 | const _nameValueMap = { 18 | _sec_of_day_str: _sec_of_day, 19 | _src_of_3_day_str: _src_of_3_day, 20 | _src_of_week_str: _src_of_week, 21 | }; 22 | 23 | const _valueNameMap = { 24 | _sec_of_day: _sec_of_day_str, 25 | _src_of_3_day: _src_of_3_day_str, 26 | _src_of_week: _src_of_week_str, 27 | }; 28 | 29 | int _value = 0; 30 | 31 | Future initCacheTime() async { 32 | final time = await api.loadProperty(k: _propertyKey); 33 | if (time.isEmpty) { 34 | _value = _src_of_week; 35 | } else { 36 | _value = int.parse(time); 37 | } 38 | if (_value > 0) { 39 | await api.cleanCache(time: _value); 40 | } 41 | } 42 | 43 | String cacheTimeName(BuildContext context) { 44 | if (_value == 0) { 45 | return "不清理"; 46 | } 47 | String? name = _valueNameMap[_value]; 48 | if (name != null) { 49 | return name; 50 | } 51 | return "$_value SEC"; 52 | } 53 | 54 | Future chooseAutoClean(BuildContext context) async { 55 | int? choose = await chooseMapDialog(context, 56 | title: "缓存保留时间", 57 | values: _nameValueMap.map((key, value) => MapEntry(key, value))); 58 | if (choose != null) { 59 | await api.saveProperty(k: _propertyKey, v: "$choose"); 60 | _value = choose; 61 | } 62 | } 63 | 64 | Widget cacheTimeNameSetting() { 65 | return StatefulBuilder( 66 | builder: (BuildContext context, void Function(void Function()) setState) { 67 | return ListTile( 68 | title: const Text("缓存保留时间"), 69 | subtitle: Text(cacheTimeName(context)), 70 | onTap: () async { 71 | await chooseAutoClean(context); 72 | setState(() {}); 73 | }, 74 | ); 75 | }, 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /lib/configs/chapter_order_newest.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../src/rust/api/api.dart' as api; 4 | import '../../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | const _propertyName = "chapterOrderNewest"; 8 | late bool _chapterOrderNewest; 9 | 10 | Future initChapterOrderNewest() async { 11 | _chapterOrderNewest = false; 12 | final st = await api.loadProperty(k: _propertyName); 13 | if (st.isNotEmpty) { 14 | try { 15 | _chapterOrderNewest = bool.parse(st); 16 | } catch (e) {} 17 | } 18 | } 19 | 20 | bool get currentChapterOrderNewest => _chapterOrderNewest; 21 | 22 | Future chooseChapterOrderNewest(BuildContext context) async { 23 | final Map map = {}; 24 | map["是"] = true; 25 | map["否"] = false; 26 | final newChapterOrderNewest = await chooseMapDialog( 27 | context, 28 | title: "详情展示章节排序反转", 29 | values: map, 30 | ); 31 | if (newChapterOrderNewest != null) { 32 | await api.saveProperty(k: _propertyName, v: "$newChapterOrderNewest"); 33 | _chapterOrderNewest = newChapterOrderNewest; 34 | } 35 | } 36 | 37 | Widget chapterOrderNewestSwitch() { 38 | return StatefulBuilder( 39 | builder: (BuildContext context, void Function(void Function()) setState) { 40 | return SwitchListTile( 41 | title: const Text("详情展示章节排序反转"), 42 | value: currentChapterOrderNewest, 43 | onChanged: (value) async { 44 | await api.saveProperty(k: _propertyName, v: "$value"); 45 | setState(() { 46 | _chapterOrderNewest = value; 47 | }); 48 | }, 49 | ); 50 | }, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/configs/collect_ordering.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/lib/configs/collect_ordering.dart -------------------------------------------------------------------------------- /lib/configs/comic_grid_columns.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:event/event.dart'; 3 | import '../src/rust/api/api.dart' as api; 4 | 5 | const _minColumns = 2; 6 | const _maxColumns = 10; 7 | const _propertyName = "comic_grid_columns"; 8 | 9 | late int _comicGridColumns = 3; // 默认3列 10 | 11 | int get currentComicGridColumns => _comicGridColumns; 12 | 13 | class ComicGridColumnsEventArgs extends EventArgs { 14 | final int columns; 15 | ComicGridColumnsEventArgs(this.columns); 16 | } 17 | 18 | final Event comicGridColumnsEvent = Event(); 19 | 20 | Future initComicGridColumns() async { 21 | var value = await api.loadProperty(k: _propertyName); 22 | if (value == null || value.isEmpty) { 23 | await api.saveProperty(k: _propertyName, v: _comicGridColumns.toString()); 24 | } else { 25 | var columns = int.tryParse(value) ?? _comicGridColumns; 26 | // 确保列数在有效范围内 27 | columns = columns.clamp(_minColumns, _maxColumns); 28 | _comicGridColumns = columns; 29 | } 30 | comicGridColumnsEvent.broadcast(ComicGridColumnsEventArgs(_comicGridColumns)); 31 | } 32 | 33 | Future chooseComicGridColumns(BuildContext context) async { 34 | var result = await showDialog( 35 | context: context, 36 | builder: (BuildContext context) { 37 | return AlertDialog( 38 | backgroundColor: const Color(0xAA000000), 39 | title: const Text( 40 | "选择网格列数", 41 | style: TextStyle(color: Colors.white), 42 | ), 43 | content: Column( 44 | mainAxisSize: MainAxisSize.min, 45 | children: [ 46 | for (var i = _minColumns; i <= _maxColumns; i++) 47 | ListTile( 48 | title: Text( 49 | "$i列", 50 | style: const TextStyle(color: Colors.white), 51 | ), 52 | onTap: () { 53 | Navigator.of(context).pop(i); 54 | }, 55 | ), 56 | ], 57 | ), 58 | ); 59 | }, 60 | ); 61 | if (result != null) { 62 | await api.saveProperty(k: _propertyName, v: result.toString()); 63 | _comicGridColumns = result; 64 | comicGridColumnsEvent.broadcast(ComicGridColumnsEventArgs(_comicGridColumns)); 65 | } 66 | } 67 | 68 | Widget comicGridColumnsSetting(BuildContext context) { 69 | return ListTile( 70 | title: const Text( 71 | "网格列数", 72 | ), 73 | subtitle: Text( 74 | "${currentComicGridColumns}列", 75 | ), 76 | onTap: () => chooseComicGridColumns(context), 77 | ); 78 | } -------------------------------------------------------------------------------- /lib/configs/comic_pager_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:event/event.dart'; 3 | import '../src/rust/api/api.dart' as api; 4 | 5 | enum ComicPagerType { 6 | grid, // 多列网格 7 | list, // 详情列表 8 | } 9 | 10 | const _propertyName = "comic_pager_type"; 11 | 12 | ComicPagerType _comicPagerType = ComicPagerType.grid; 13 | 14 | ComicPagerType get currentComicPagerType => _comicPagerType; 15 | 16 | class ComicPagerTypeEventArgs extends EventArgs { 17 | final ComicPagerType type; 18 | ComicPagerTypeEventArgs(this.type); 19 | } 20 | 21 | final Event comicPagerTypeEvent = Event(); 22 | 23 | Future initComicPagerType() async { 24 | var value = await api.loadProperty(k: _propertyName); 25 | if (value == null || value.isEmpty) { 26 | _comicPagerType = ComicPagerType.grid; 27 | } else { 28 | _comicPagerType = ComicPagerType.values.firstWhere( 29 | (e) => e.name == value, 30 | orElse: () => ComicPagerType.grid, 31 | ); 32 | } 33 | comicPagerTypeEvent.broadcast(ComicPagerTypeEventArgs(_comicPagerType)); 34 | } 35 | 36 | Future chooseComicPagerType(BuildContext context) async { 37 | var result = await showDialog( 38 | context: context, 39 | builder: (BuildContext context) { 40 | return AlertDialog( 41 | backgroundColor: const Color(0xAA000000), 42 | title: const Text( 43 | "选择漫画列表显示方式", 44 | style: TextStyle(color: Colors.white), 45 | ), 46 | content: Column( 47 | mainAxisSize: MainAxisSize.min, 48 | children: [ 49 | ListTile( 50 | title: const Text( 51 | "多列网格", 52 | style: TextStyle(color: Colors.white), 53 | ), 54 | onTap: () { 55 | Navigator.of(context).pop(ComicPagerType.grid); 56 | }, 57 | ), 58 | ListTile( 59 | title: const Text( 60 | "详情列表", 61 | style: TextStyle(color: Colors.white), 62 | ), 63 | onTap: () { 64 | Navigator.of(context).pop(ComicPagerType.list); 65 | }, 66 | ), 67 | ], 68 | ), 69 | ); 70 | }, 71 | ); 72 | if (result != null) { 73 | await api.saveProperty(k: _propertyName, v: result.name); 74 | _comicPagerType = result; 75 | comicPagerTypeEvent.broadcast(ComicPagerTypeEventArgs(_comicPagerType)); 76 | } 77 | } 78 | 79 | String comicPagerTypeName(ComicPagerType type, BuildContext context) { 80 | switch (type) { 81 | case ComicPagerType.grid: 82 | return "多列网格"; 83 | case ComicPagerType.list: 84 | return "详情列表"; 85 | } 86 | } 87 | 88 | Widget comicPagerTypeSetting(BuildContext context) { 89 | return ListTile( 90 | title: const Text( 91 | "漫画列表显示方式", 92 | ), 93 | subtitle: Text( 94 | comicPagerTypeName(currentComicPagerType, context), 95 | ), 96 | onTap: () => chooseComicPagerType(context), 97 | ); 98 | } -------------------------------------------------------------------------------- /lib/configs/configs.dart: -------------------------------------------------------------------------------- 1 | import 'package:kobi/configs/app_orientation.dart'; 2 | import 'package:kobi/configs/app_theme.dart'; 3 | import 'package:kobi/configs/comic_grid_columns.dart'; 4 | import 'package:kobi/configs/comic_pager_type.dart'; 5 | import 'package:kobi/configs/list_volume.dart'; 6 | import 'package:kobi/configs/login.dart'; 7 | import 'package:kobi/configs/no_pager_animation.dart'; 8 | import 'package:kobi/configs/proxy.dart'; 9 | import 'package:kobi/configs/reader_controller_type.dart'; 10 | import 'package:kobi/configs/reader_direction.dart'; 11 | import 'package:kobi/configs/reader_slider_position.dart'; 12 | import 'package:kobi/configs/reader_type.dart'; 13 | import 'package:kobi/configs/versions.dart'; 14 | 15 | import 'api_host.dart'; 16 | import 'cache_time.dart'; 17 | import 'chapter_order_newest.dart'; 18 | import 'collect_ordering.dart'; 19 | 20 | Future initConfigs() async { 21 | await initAppOrientation(); 22 | await initAppTheme(); 23 | await initApiHost(); 24 | await initProxy(); 25 | await initCacheTime(); 26 | await initReaderControllerType(); 27 | await initReaderDirection(); 28 | await initReaderSliderPosition(); 29 | await initReaderType(); 30 | await initLogin(); 31 | await initVersion(); 32 | await initNoPagerAnimation(); 33 | await initChapterOrderNewest(); 34 | await initComicPagerType(); 35 | await initComicGridColumns(); 36 | await initListVolume(); 37 | autoCheckNewVersion(); 38 | } 39 | -------------------------------------------------------------------------------- /lib/configs/list_volume.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../src/rust/api/api.dart' as api; 4 | import '../../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | const _propertyName = "listVolume"; 8 | late bool _listVolume; 9 | 10 | Future initListVolume() async { 11 | _listVolume = false; 12 | final st = await api.loadProperty(k: _propertyName); 13 | if (st.isNotEmpty) { 14 | try { 15 | _listVolume = bool.parse(st); 16 | } catch (e) {} 17 | } 18 | } 19 | 20 | bool get currentListVolume => _listVolume; 21 | 22 | Future chooseListVolume(BuildContext context) async { 23 | final Map map = {}; 24 | map["是"] = true; 25 | map["否"] = false; 26 | final newListVolume = await chooseMapDialog( 27 | context, 28 | title: "是否启动音量翻页", 29 | values: map, 30 | ); 31 | if (newListVolume != null) { 32 | await api.saveProperty(k: _propertyName, v: "$newListVolume"); 33 | _listVolume = newListVolume; 34 | } 35 | } 36 | 37 | Widget listVolumeSwitch() { 38 | return StatefulBuilder( 39 | builder: (BuildContext context, void Function(void Function()) setState) { 40 | return SwitchListTile( 41 | title: const Text("启动音量翻页"), 42 | value: currentListVolume, 43 | onChanged: (value) async { 44 | await api.saveProperty(k: _propertyName, v: "$value"); 45 | setState(() { 46 | _listVolume = value; 47 | }); 48 | }, 49 | ); 50 | }, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/configs/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:event/event.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:kobi/screens/components/commons.dart'; 4 | import '../src/rust/api/api.dart' as api; 5 | import '../src/rust/udto.dart'; 6 | 7 | bool _logging = true; 8 | 9 | bool get logging => _logging; 10 | 11 | final loginEvent = Event(); 12 | 13 | UILoginState _loginState = const UILoginState( 14 | state: 0, 15 | message: "", 16 | member: null, 17 | ); 18 | 19 | UILoginState get loginState => _loginState; 20 | 21 | Future initLogin() async { 22 | _logging = true; 23 | loginEvent.broadcast(); 24 | _loginState = await api.initLoginState(); 25 | _logging = false; 26 | loginEvent.broadcast(); 27 | } 28 | 29 | Future login(String username, String password) async { 30 | _logging = true; 31 | loginEvent.broadcast(); 32 | _loginState = await api.login(username: username, password: password); 33 | _logging = false; 34 | loginEvent.broadcast(); 35 | } 36 | 37 | Future register(BuildContext context, String username, String password) async { 38 | _logging = true; 39 | loginEvent.broadcast(); 40 | final result = await api.register(username: username, password: password); 41 | if (result.state == 1) { 42 | defaultToast(context, "注册成功, 请登录", seconds: 10); 43 | } else { 44 | defaultToast(context, result.message, seconds: 10); 45 | } 46 | _logging = false; 47 | loginEvent.broadcast(); 48 | } 49 | -------------------------------------------------------------------------------- /lib/configs/map_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:event/event.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:kobi/src/rust/api/api.dart'; 5 | 6 | import '../screens/components/commons.dart'; 7 | 8 | class MapConfig { 9 | late final Map valueName; 10 | late final Map nameValue; 11 | final String propertyName; 12 | final String propertyKey; 13 | late String value; 14 | late final Event changeEvent; 15 | final String defaultValue; 16 | 17 | MapConfig({ 18 | required Map valueName, 19 | required this.defaultValue, 20 | required this.propertyKey, 21 | required this.propertyName, 22 | }) { 23 | this.valueName = valueName; 24 | this.nameValue = {}; 25 | valueName.forEach((key, value) { 26 | nameValue[value] = key; 27 | }); 28 | this.changeEvent = Event(); 29 | if (!valueName.containsKey(defaultValue)) { 30 | throw ArgumentError("defaultValue not in valueName"); 31 | } 32 | } 33 | 34 | Future initConfig() async { 35 | value = await loadProperty(k: propertyKey); 36 | if (!valueName.containsKey(value)) { 37 | value = defaultValue; 38 | await saveProperty(k: propertyKey, v: value); 39 | } 40 | } 41 | 42 | Widget configWidget(BuildContext context) { 43 | return StatefulBuilder( 44 | builder: (BuildContext context, void Function(void Function()) setState) { 45 | return ListTile( 46 | title: Text(propertyName), 47 | subtitle: Text(valueName[value] ?? ""), 48 | onTap: () async { 49 | String? result = await chooseMapDialog( 50 | context, 51 | title: propertyName, 52 | values: nameValue, 53 | ); 54 | if (result != null) { 55 | await saveProperty(k: propertyKey, v: result); 56 | value = result; 57 | setState(() {}); 58 | changeEvent.broadcast(); 59 | } 60 | }, 61 | ); 62 | }, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/configs/no_pager_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../src/rust/api/api.dart' as api; 4 | import '../../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | const _propertyName = "noPagerAnimation"; 8 | late bool _noPagerAnimation; 9 | 10 | Future initNoPagerAnimation() async { 11 | _noPagerAnimation = false; 12 | final st = await api.loadProperty(k: _propertyName); 13 | if (st.isNotEmpty) { 14 | try { 15 | _noPagerAnimation = bool.parse(st); 16 | } catch (e) {} 17 | } 18 | } 19 | 20 | bool get currentNoPagerAnimation => _noPagerAnimation; 21 | 22 | Future chooseNoPagerAnimation(BuildContext context) async { 23 | final Map map = {}; 24 | map["是"] = true; 25 | map["否"] = false; 26 | final newNoPagerAnimation = await chooseMapDialog( 27 | context, 28 | title: "是否禁用翻页动画", 29 | values: map, 30 | ); 31 | if (newNoPagerAnimation != null) { 32 | await api.saveProperty(k: _propertyName, v: "$newNoPagerAnimation"); 33 | _noPagerAnimation = newNoPagerAnimation; 34 | } 35 | } 36 | 37 | Widget noPagerAnimationSwitch() { 38 | return StatefulBuilder( 39 | builder: (BuildContext context, void Function(void Function()) setState) { 40 | return SwitchListTile( 41 | title: const Text("禁用翻页动画"), 42 | value: currentNoPagerAnimation, 43 | onChanged: (value) async { 44 | await api.saveProperty(k: _propertyName, v: "$value"); 45 | setState(() { 46 | _noPagerAnimation = value; 47 | }); 48 | }, 49 | ); 50 | }, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/configs/proxy.dart: -------------------------------------------------------------------------------- 1 | /// 代理设置 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../src/rust/api/api.dart' as api; 6 | import '../src/rust/udto.dart'; 7 | import '../screens/components/commons.dart'; 8 | 9 | late String _currentProxy; 10 | 11 | Future initProxy() async { 12 | _currentProxy = await api.getProxy(); 13 | return null; 14 | } 15 | 16 | String currentProxyName() { 17 | return _currentProxy == "" ? "未设置" : _currentProxy; 18 | } 19 | 20 | Future inputProxy(BuildContext context) async { 21 | String? input = await displayTextInputDialog( 22 | context, 23 | src: _currentProxy, 24 | title: '代理服务器', 25 | hint: '请输入代理服务器', 26 | desc: " ( 例如 socks5://127.0.0.1:1080/ ) ", 27 | ); 28 | if (input != null) { 29 | await api.setProxy(proxy: input); 30 | _currentProxy = input; 31 | } 32 | } 33 | 34 | Widget proxySetting() { 35 | return StatefulBuilder( 36 | builder: (BuildContext context, void Function(void Function()) setState) { 37 | return ListTile( 38 | title: const Text("代理服务器"), 39 | subtitle: Text(currentProxyName()), 40 | onTap: () async { 41 | await inputProxy(context); 42 | setState(() {}); 43 | }, 44 | ); 45 | }, 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /lib/configs/reader_controller_type.dart: -------------------------------------------------------------------------------- 1 | /// 全屏操作 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../../src/rust/api/api.dart' as api; 6 | import '../../src/rust/udto.dart'; 7 | import '../screens/components/commons.dart'; 8 | 9 | enum ReaderControllerType { 10 | touchOnce, 11 | controller, 12 | touchDouble, 13 | touchDoubleOnceNext, 14 | threeArea, 15 | } 16 | 17 | Map _readerControllerTypeMap = { 18 | "点击屏幕一次全屏": ReaderControllerType.touchOnce, 19 | "使用控制器全屏": ReaderControllerType.controller, 20 | "双击屏幕全屏": ReaderControllerType.touchDouble, 21 | "双击屏幕全屏 + 单击屏幕下一页": ReaderControllerType.touchDoubleOnceNext, 22 | "将屏幕划分成三个区域 (上一页, 下一页, 全屏)": ReaderControllerType.threeArea, 23 | }; 24 | 25 | const _defaultController = ReaderControllerType.touchOnce; 26 | const _propertyName = "reader_controller_type"; 27 | late ReaderControllerType _readerControllerType; 28 | 29 | Future initReaderControllerType() async { 30 | _readerControllerType = _readerControllerTypeFromString( 31 | await api.loadProperty(k: _propertyName), 32 | ); 33 | } 34 | 35 | ReaderControllerType get currentReaderControllerType => _readerControllerType; 36 | 37 | ReaderControllerType _readerControllerTypeFromString(String string) { 38 | for (var value in ReaderControllerType.values) { 39 | if (string == value.toString()) { 40 | return value; 41 | } 42 | } 43 | return _defaultController; 44 | } 45 | 46 | String currentReaderControllerTypeName() { 47 | for (var e in _readerControllerTypeMap.entries) { 48 | if (e.value == _readerControllerType) { 49 | return e.key; 50 | } 51 | } 52 | return ''; 53 | } 54 | 55 | Future chooseReaderControllerType(BuildContext context) async { 56 | ReaderControllerType? result = await chooseMapDialog( 57 | context, 58 | title: "选择操控方式", 59 | values: _readerControllerTypeMap, 60 | ); 61 | if (result != null) { 62 | await api.saveProperty(k: _propertyName, v: result.toString()); 63 | _readerControllerType = result; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/configs/reader_direction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../src/rust/api/api.dart' as api; 4 | import '../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | enum ReaderDirection { 8 | topToBottom, 9 | leftToRight, 10 | rightToLeft, 11 | } 12 | 13 | const _propertyName = "readerDirection"; 14 | late ReaderDirection _readerDirection; 15 | 16 | Future initReaderDirection() async { 17 | _readerDirection = _fromString(await api.loadProperty(k: _propertyName)); 18 | } 19 | 20 | ReaderDirection _fromString(String valueForm) { 21 | for (var value in ReaderDirection.values) { 22 | if (value.toString() == valueForm) { 23 | return value; 24 | } 25 | } 26 | return ReaderDirection.values.first; 27 | } 28 | 29 | ReaderDirection get currentReaderDirection => _readerDirection; 30 | 31 | String readerDirectionName(ReaderDirection direction, BuildContext context) { 32 | switch (direction) { 33 | case ReaderDirection.topToBottom: 34 | return "从上到下"; 35 | case ReaderDirection.leftToRight: 36 | return "从左到右"; 37 | case ReaderDirection.rightToLeft: 38 | return "从右到左"; 39 | } 40 | } 41 | 42 | Future chooseReaderDirection(BuildContext context) async { 43 | final Map map = {}; 44 | for (var element in ReaderDirection.values) { 45 | map[readerDirectionName(element, context)] = element; 46 | } 47 | final newReaderDirection = await chooseMapDialog( 48 | context, 49 | title: "请选择阅读器方向", 50 | values: map, 51 | ); 52 | if (newReaderDirection != null) { 53 | await api.saveProperty(k: _propertyName, v: "$newReaderDirection"); 54 | _readerDirection = newReaderDirection; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/configs/reader_slider_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../src/rust/api/api.dart' as api; 4 | import '../screens/components/commons.dart'; 5 | 6 | enum ReaderSliderPosition { 7 | bottom, 8 | right, 9 | left, 10 | } 11 | 12 | const _positionNames = { 13 | ReaderSliderPosition.bottom: '下方', 14 | ReaderSliderPosition.right: '右侧', 15 | ReaderSliderPosition.left: '左侧', 16 | }; 17 | 18 | const _propertyName = "reader_slider_position"; 19 | late ReaderSliderPosition _readerSliderPosition; 20 | 21 | Future initReaderSliderPosition() async { 22 | _readerSliderPosition = _readerSliderPositionFromString( 23 | await api.loadProperty(k: _propertyName), 24 | ); 25 | } 26 | 27 | ReaderSliderPosition _readerSliderPositionFromString(String str) { 28 | for (var value in ReaderSliderPosition.values) { 29 | if (str == value.toString()) return value; 30 | } 31 | return ReaderSliderPosition.bottom; 32 | } 33 | 34 | ReaderSliderPosition get currentReaderSliderPosition => _readerSliderPosition; 35 | 36 | String get currentReaderSliderPositionName => 37 | _positionNames[_readerSliderPosition] ?? ""; 38 | 39 | Future chooseReaderSliderPosition(BuildContext context) async { 40 | Map map = {}; 41 | _positionNames.forEach((key, value) { 42 | map[value] = key; 43 | }); 44 | ReaderSliderPosition? result = await chooseMapDialog( 45 | context, 46 | title: "选择滑动条位置", 47 | values: map, 48 | ); 49 | if (result != null) { 50 | await api.saveProperty(k: _propertyName, v: result.toString()); 51 | _readerSliderPosition = result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/configs/reader_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../src/rust/api/api.dart' as api; 4 | import '../../src/rust/udto.dart'; 5 | import '../screens/components/commons.dart'; 6 | 7 | enum ReaderType { 8 | webtoon, 9 | gallery, 10 | webToonFreeZoom, 11 | twoPageGallery, 12 | } 13 | 14 | const _propertyName = "readerType"; 15 | late ReaderType _readerType; 16 | 17 | Future initReaderType() async { 18 | _readerType = _fromString(await api.loadProperty(k: _propertyName)); 19 | } 20 | 21 | ReaderType _fromString(String valueForm) { 22 | for (var value in ReaderType.values) { 23 | if (value.toString() == valueForm) { 24 | return value; 25 | } 26 | } 27 | return ReaderType.values.first; 28 | } 29 | 30 | ReaderType get currentReaderType => _readerType; 31 | 32 | String readerTypeName(ReaderType type, BuildContext context) { 33 | switch (type) { 34 | case ReaderType.webtoon: 35 | return "WebToon"; 36 | case ReaderType.gallery: 37 | return "相册"; 38 | case ReaderType.webToonFreeZoom: 39 | return "自由放大滚动 无法翻页"; 40 | case ReaderType.twoPageGallery: 41 | return "双页相册"; 42 | } 43 | } 44 | 45 | Future chooseReaderType(BuildContext context) async { 46 | final Map map = {}; 47 | for (var element in ReaderType.values) { 48 | map[readerTypeName(element, context)] = element; 49 | } 50 | final newReaderType = await chooseMapDialog( 51 | context, 52 | title: "请选择阅读器类型", 53 | values: map, 54 | ); 55 | if (newReaderType != null) { 56 | await api.saveProperty(k: _propertyName, v: "$newReaderType"); 57 | _readerType = newReaderType; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/configs/versions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:convert'; 3 | import 'package:event/event.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart' show rootBundle; 6 | import '../../src/rust/api/api.dart' as api; 7 | import '../../src/rust/udto.dart'; 8 | import '../screens/components/commons.dart'; 9 | 10 | const _versionUrl = 11 | "https://api.github.com/repos/niuhuan/kobi/releases/latest"; 12 | const _versionAssets = 'lib/assets/version.txt'; 13 | RegExp _versionExp = RegExp(r"^v\d+\.\d+.\d+$"); 14 | 15 | late String _version; 16 | String? _latestVersion; 17 | String? _latestVersionInfo; 18 | 19 | Future initVersion() async { 20 | // 当前版本 21 | try { 22 | _version = (await rootBundle.loadString(_versionAssets)).trim(); 23 | } catch (e) { 24 | _version = "dirty"; 25 | } 26 | } 27 | 28 | var versionEvent = Event(); 29 | 30 | String currentVersion() { 31 | return _version; 32 | } 33 | 34 | String? get latestVersion => _latestVersion; 35 | 36 | String? latestVersionInfo() { 37 | return _latestVersionInfo; 38 | } 39 | 40 | Future autoCheckNewVersion() { 41 | return _versionCheck(); 42 | } 43 | 44 | Future manualCheckNewVersion(BuildContext context) async { 45 | try { 46 | defaultToast(context, "检查更新中"); 47 | await _versionCheck(); 48 | defaultToast(context, "检查更新成功"); 49 | } catch (e) { 50 | defaultToast(context, "检查更新失败 : $e"); 51 | } 52 | } 53 | 54 | bool dirtyVersion() { 55 | return !_versionExp.hasMatch(_version); 56 | } 57 | 58 | // maybe exception 59 | Future _versionCheck() async { 60 | if (_versionExp.hasMatch(_version)) { 61 | var json = jsonDecode(await api.httpGet(url: _versionUrl)); 62 | if (json["name"] != null) { 63 | String latestVersion = (json["name"]); 64 | if (latestVersion != _version) { 65 | _latestVersion = latestVersion; 66 | _latestVersionInfo = json["body"] ?? ""; 67 | } 68 | } 69 | } // else dirtyVersion 70 | versionEvent.broadcast(); 71 | print("$_latestVersion"); 72 | } 73 | -------------------------------------------------------------------------------- /lib/cross.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'src/rust/api/api.dart' as api; 5 | import 'package:url_launcher/url_launcher.dart'; 6 | 7 | const cross = Cross._(); 8 | 9 | class Cross { 10 | const Cross._(); 11 | 12 | static const _channel = MethodChannel("cross"); 13 | 14 | Future root() async { 15 | if (Platform.isAndroid || Platform.isIOS) { 16 | return await _channel.invokeMethod("root"); 17 | } 18 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 19 | return await api.desktopRoot(); 20 | } 21 | throw "没有适配的平台"; 22 | } 23 | 24 | Future saveImageToGallery(String path) async { 25 | if (Platform.isAndroid || Platform.isIOS) { 26 | return await _channel.invokeMethod("saveImageToGallery", path); 27 | } 28 | throw "没有适配的平台"; 29 | } 30 | 31 | Future androidGetVersion() async { 32 | if (Platform.isAndroid) { 33 | return await _channel.invokeMethod("androidGetVersion"); 34 | } 35 | return 0; 36 | } 37 | 38 | Future> loadAndroidModes() async { 39 | return List.of(await _channel.invokeMethod("androidGetModes")) 40 | .map((e) => "$e") 41 | .toList(); 42 | } 43 | 44 | Future setAndroidMode(String androidDisplayMode) { 45 | return _channel 46 | .invokeMethod("androidSetMode", {"mode": androidDisplayMode}); 47 | } 48 | 49 | Future androidAppInfo() { 50 | return _channel.invokeMethod("androidAppInfo", ""); 51 | } 52 | } 53 | 54 | /// 打开web页面 55 | Future openUrl(String url) async { 56 | if (await canLaunch(url)) { 57 | await launch( 58 | url, 59 | forceSafariVC: false, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/screens/app_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:kobi/screens/components/badged.dart'; 3 | import 'rank_screen.dart'; 4 | import 'user_screen.dart'; 5 | import 'discovery_screen.dart'; 6 | 7 | class AppScreen extends StatefulWidget { 8 | const AppScreen({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _AppScreenState(); 12 | } 13 | 14 | class _AppScreenState extends State { 15 | var _pageIndex = 1; 16 | late final _pageController = PageController(initialPage: _pageIndex); 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | } 22 | 23 | @override 24 | void dispose() { 25 | _pageController.dispose(); 26 | super.dispose(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final theme = Theme.of(context); 32 | return Scaffold( 33 | body: PageView( 34 | physics: const NeverScrollableScrollPhysics(), 35 | allowImplicitScrolling: false, 36 | controller: _pageController, 37 | onPageChanged: (index) { 38 | /// 重新渲染导航 39 | setState(() { 40 | _pageIndex = index; 41 | }); 42 | }, 43 | children: _screens.map((e) => e.screen).toList(), 44 | ), 45 | bottomNavigationBar: BottomNavigationBar( 46 | items: _screens 47 | .map((e) => BottomNavigationBarItem( 48 | label: e.title, 49 | icon: e.title == "偏好" ? VersionBadged(child: Icon(e.icon)) :Icon(e.icon), 50 | tooltip: "", 51 | )) 52 | .toList(), 53 | currentIndex: _pageIndex, 54 | onTap: _onItemTapped, 55 | selectedLabelStyle: const TextStyle( 56 | fontSize: 12, 57 | ), 58 | unselectedLabelStyle: const TextStyle( 59 | fontSize: 12, 60 | ), 61 | selectedItemColor: theme.tabBarTheme.labelColor, 62 | unselectedItemColor: theme.tabBarTheme.unselectedLabelColor, 63 | showSelectedLabels: true, 64 | showUnselectedLabels: true, 65 | iconSize: 24, 66 | type: BottomNavigationBarType.fixed, 67 | ), 68 | ); 69 | } 70 | 71 | /// 导航内容 72 | late final List _screens = const [ 73 | AppScreenData( 74 | RankScreen(), 75 | '排行', 76 | Icons.local_fire_department_outlined, 77 | ), 78 | AppScreenData( 79 | DiscoveryScreen(), 80 | '发现', 81 | Icons.filter_list_sharp, 82 | ), 83 | AppScreenData( 84 | UserScreen(), 85 | '偏好', 86 | Icons.account_box_outlined, 87 | ), 88 | ]; 89 | 90 | void _onItemTapped(int value) { 91 | setState(() { 92 | _pageIndex = value; 93 | }); 94 | _pageController.jumpToPage( 95 | value, 96 | ); 97 | } 98 | } 99 | 100 | class AppScreenData { 101 | final Widget screen; 102 | final String title; 103 | final IconData icon; 104 | 105 | const AppScreenData(this.screen, this.title, this.icon); 106 | } 107 | -------------------------------------------------------------------------------- /lib/screens/comic_search_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'components/flutter_search_bar_base.dart' as sb; 3 | 4 | import '../src/rust/api/api.dart' as api; 5 | import 'components/comic_card.dart'; 6 | import 'components/comic_pager.dart'; 7 | 8 | class ComicSearchScreen extends StatefulWidget { 9 | final String initialQuery; 10 | 11 | const ComicSearchScreen({super.key, required this.initialQuery}); 12 | 13 | @override 14 | _ComicSearchScreenState createState() => _ComicSearchScreenState(); 15 | } 16 | 17 | class _ComicSearchScreenState extends State { 18 | late var _query = widget.initialQuery; 19 | 20 | late final _searchBar = sb.SearchBar( 21 | hintText: '搜索', 22 | inBar: false, 23 | setState: setState, 24 | onSubmitted: (value) { 25 | if (value.isNotEmpty) { 26 | setState(() { 27 | _query = value; 28 | }); 29 | } 30 | }, 31 | buildDefaultAppBar: _appBar, 32 | ); 33 | 34 | AppBar _appBar(BuildContext context) { 35 | return AppBar( 36 | title: Text(_query), 37 | actions: [ 38 | _searchBar.getSearchAction(context), 39 | ], 40 | ); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Scaffold( 46 | key: Key("search_screen:$_query"), 47 | appBar: _searchBar.build(context), 48 | body: ComicPager(fetcher: (offset, limit) async { 49 | final result = await api.comicSearch( 50 | qType: "", q: _query, offset: offset, limit: limit); 51 | return CommonPage( 52 | list: result.list 53 | .map((e) => CommonComicInfo( 54 | author: e.author, 55 | cover: e.cover, 56 | imgType: e.imgType, 57 | name: e.name, 58 | pathWord: e.pathWord, 59 | popular: e.popular, 60 | males: e.males, 61 | females: e.females, 62 | )) 63 | .toList(), 64 | total: result.total, 65 | limit: result.limit, 66 | offset: result.offset, 67 | ); 68 | }), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/screens/components/android_version.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:kobi/cross.dart'; 4 | 5 | int _androidVersion = 0; 6 | 7 | int get androidVersion => _androidVersion; 8 | 9 | Future initAndroidVersion() async { 10 | _androidVersion = await cross.androidGetVersion(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/screens/components/badged.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../configs/versions.dart'; 3 | 4 | // 提示信息, 组件右上角的小红点 5 | class Badged extends StatelessWidget { 6 | final String? badge; 7 | final Widget child; 8 | 9 | const Badged({Key? key, required this.child, this.badge}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | if (badge == null) { 14 | return child; 15 | } 16 | return Stack( 17 | children: [ 18 | child, 19 | Positioned( 20 | right: 0, 21 | child: Container( 22 | padding: const EdgeInsets.all(1), 23 | decoration: BoxDecoration( 24 | color: Colors.red, 25 | borderRadius: BorderRadius.circular(6), 26 | ), 27 | constraints: const BoxConstraints( 28 | minWidth: 12, 29 | minHeight: 12, 30 | ), 31 | child: Text( 32 | badge!, 33 | style: const TextStyle( 34 | color: Colors.white, 35 | fontSize: 8, 36 | ), 37 | textAlign: TextAlign.center, 38 | ), 39 | ), 40 | ), 41 | ], 42 | ); 43 | } 44 | } 45 | 46 | class VersionBadged extends StatefulWidget { 47 | final Widget child; 48 | 49 | const VersionBadged({required this.child, Key? key}) : super(key: key); 50 | 51 | @override 52 | State createState() => _VersionBadgedState(); 53 | } 54 | 55 | class _VersionBadgedState extends State { 56 | @override 57 | void initState() { 58 | versionEvent.subscribe(_onVersion); 59 | super.initState(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | versionEvent.unsubscribe(_onVersion); 65 | super.dispose(); 66 | } 67 | 68 | void _onVersion(dynamic a) { 69 | setState(() {}); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Badged( 75 | child: widget.child, 76 | badge: latestVersion == null ? null : "1", 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/screens/components/content_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContentLoading extends StatelessWidget { 4 | final String label; 5 | final bool sq; 6 | 7 | const ContentLoading({Key? key, this.label = "加载中", this.sq = false}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return LayoutBuilder( 12 | builder: (BuildContext context, BoxConstraints constraints) { 13 | var width = constraints.maxWidth; 14 | var height = constraints.maxHeight; 15 | if (sq) { 16 | height = width; 17 | } 18 | var min = width < height ? width : height; 19 | var theme = Theme.of(context); 20 | return SizedBox( 21 | width: width, 22 | height: height, 23 | child: Center( 24 | child: Column( 25 | children: [ 26 | Expanded(child: Container()), 27 | SizedBox( 28 | width: min / 2, 29 | height: min / 2, 30 | child: CircularProgressIndicator( 31 | color: theme.colorScheme.secondary, 32 | backgroundColor: Colors.grey[100], 33 | ), 34 | ), 35 | Container(height: min / 10), 36 | Text(label, style: TextStyle(fontSize: min / 15)), 37 | Expanded(child: Container()), 38 | ], 39 | ), 40 | ), 41 | ); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/screens/components/error_types.dart: -------------------------------------------------------------------------------- 1 | const ERROR_TYPE_NETWORK = "NETWORK_ERROR"; 2 | const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR"; 3 | const ERROR_TYPE_TIME = "TIME_ERROR"; 4 | const ERROR_TYPE_UNDER_REVIEW = "UNDER_VIEW_ERROR"; 5 | 6 | // 错误的类型, 方便照展示和谐的提示 7 | String errorType(String error) { 8 | if (error.contains("timeout") || 9 | error.contains("connection refused") || 10 | error.contains("deadline") || 11 | error.contains("connection abort")) { 12 | return ERROR_TYPE_NETWORK; 13 | } 14 | if (error.contains("permission denied")) { 15 | return ERROR_TYPE_PERMISSION; 16 | } 17 | if (error.contains("time is not synchronize")) { 18 | return ERROR_TYPE_TIME; 19 | } 20 | if (error.contains("under review")) { 21 | return ERROR_TYPE_UNDER_REVIEW; 22 | } 23 | return ""; 24 | } 25 | -------------------------------------------------------------------------------- /lib/screens/components/fade_image_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FadeImageWidget extends StatelessWidget { 4 | 5 | final Widget child; 6 | 7 | const FadeImageWidget({super.key, required this.child}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return ShaderMask( 12 | shaderCallback: (Rect bounds) { 13 | return const RadialGradient( 14 | center: Alignment.center, 15 | radius: 0.8, 16 | colors: [ 17 | Colors.black, 18 | Colors.black, 19 | Colors.transparent, 20 | ], 21 | stops: [0.7, 0.95, 1.0], // 渐变范围 22 | ).createShader(bounds); 23 | }, 24 | blendMode: BlendMode.dstIn, // 混合模式,保留透明部分 25 | child: child, 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/screens/components/file_photo_view_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:photo_view/photo_view.dart'; 5 | import 'commons.dart'; 6 | 7 | // 预览图片 8 | class FilePhotoViewScreen extends StatelessWidget { 9 | final String filePath; 10 | 11 | const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) => Scaffold( 15 | body: Stack( 16 | children: [ 17 | GestureDetector( 18 | onLongPress: () async { 19 | String? choose = await chooseListDialog( 20 | context, 21 | title: '请选择', 22 | values: ['保存图片'], 23 | ); 24 | switch (choose) { 25 | case '保存图片': 26 | saveImageFileToGallery(context, filePath); 27 | break; 28 | } 29 | }, 30 | child: PhotoView( 31 | imageProvider: FileImage(File(filePath)), 32 | ), 33 | ), 34 | InkWell( 35 | onTap: () => Navigator.of(context).pop(), 36 | child: Container( 37 | margin: const EdgeInsets.only(top: 30), 38 | padding: const EdgeInsets.only(left: 4, right: 4), 39 | decoration: BoxDecoration( 40 | color: Colors.black.withOpacity(.75), 41 | borderRadius: const BorderRadius.only( 42 | topRight: Radius.circular(8), 43 | bottomRight: Radius.circular(8), 44 | ), 45 | ), 46 | child: const Icon(Icons.keyboard_backspace, color: Colors.white), 47 | ), 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/screens/components/image_cache_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui' as ui; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import '../../src/rust/api/api.dart' as api; 6 | import '../../src/rust/udto.dart'; 7 | import 'package:kobi/screens/components/images.dart'; 8 | 9 | class ImageCacheProvider extends ImageProvider { 10 | final String url; 11 | final String useful; 12 | final double scale; 13 | final String? extendsFieldFirst; 14 | final String? extendsFieldSecond; 15 | final String? extendsFieldThird; 16 | 17 | ImageCacheProvider({ 18 | required this.url, 19 | required this.useful, 20 | this.extendsFieldFirst, 21 | this.extendsFieldSecond, 22 | this.extendsFieldThird, 23 | this.scale = 1.0, 24 | }); 25 | 26 | @override 27 | ImageStreamCompleter loadImage( 28 | ImageCacheProvider key, ImageDecoderCallback decode) { 29 | return MultiFrameImageStreamCompleter( 30 | codec: _loadAsync(key), 31 | scale: key.scale, 32 | ); 33 | } 34 | 35 | @override 36 | Future obtainKey(ImageConfiguration configuration) { 37 | return SynchronousFuture(this); 38 | } 39 | 40 | Future _loadAsync(ImageCacheProvider key) async { 41 | assert(key == this); 42 | final path = (await api.cacheImage( 43 | cacheKey: imageUrlToCacheKey(url), 44 | url: url, 45 | useful: useful, 46 | extendsFieldFirst: extendsFieldFirst, 47 | extendsFieldSecond: extendsFieldSecond, 48 | extendsFieldThird: extendsFieldThird, 49 | )) 50 | .absPath; 51 | return ui.instantiateImageCodec( 52 | await _loadImageFile(path), 53 | ); 54 | } 55 | 56 | @override 57 | bool operator ==(dynamic other) { 58 | if (other.runtimeType != runtimeType) return false; 59 | final ImageCacheProvider typedOther = other; 60 | return url == typedOther.url && scale == typedOther.scale; 61 | } 62 | 63 | @override 64 | int get hashCode => Object.hash(url, scale); 65 | 66 | @override 67 | String toString() => '$runtimeType(' 68 | 'path: ${describeIdentity(url)},' 69 | ' scale: $scale' 70 | ')'; 71 | } 72 | 73 | Future _loadImageFile(String path) { 74 | return File(path).readAsBytes(); 75 | } 76 | -------------------------------------------------------------------------------- /lib/screens/components/router.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | final RouteObserver> routeObserver = 5 | RouteObserver>(); 6 | -------------------------------------------------------------------------------- /lib/screens/init_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:kobi/configs/configs.dart'; 3 | import 'package:kobi/screens/components/fade_image_widget.dart'; 4 | import '../cross.dart'; 5 | import '../src/rust/api/api.dart' as api; 6 | import '../src/rust/udto.dart'; 7 | import 'app_screen.dart'; 8 | 9 | class InitScreen extends StatefulWidget { 10 | const InitScreen({super.key}); 11 | 12 | @override 13 | _InitScreenState createState() => _InitScreenState(); 14 | } 15 | 16 | class _InitScreenState extends State { 17 | @override 18 | void initState() { 19 | super.initState(); 20 | init(); 21 | } 22 | 23 | Future init() async { 24 | await api.init(root: await cross.root()); 25 | await initConfigs(); 26 | Navigator.of(context) 27 | .pushReplacement(MaterialPageRoute(builder: (_) => const AppScreen())); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | body: ConstrainedBox( 34 | constraints: const BoxConstraints.expand(), 35 | child: LayoutBuilder( 36 | builder: (BuildContext context, BoxConstraints constraints) { 37 | var width = 1024; 38 | var height = 1536; 39 | var min = constraints.maxWidth > constraints.maxHeight 40 | ? constraints.maxHeight 41 | : constraints.maxWidth; 42 | var newHeight = min; 43 | var newWidth = min * (width / height); 44 | return Center( 45 | child: ShaderMask( 46 | shaderCallback: (Rect bounds) { 47 | return const LinearGradient( 48 | begin: Alignment.topCenter, 49 | end: Alignment.bottomCenter, 50 | colors: [ 51 | Colors.black, 52 | Colors.black, 53 | Colors.transparent, 54 | ], 55 | stops: [0.0, 0.95, 1.0], 56 | ).createShader(bounds); 57 | }, 58 | blendMode: BlendMode.dstIn, 59 | child: Image.asset( 60 | "lib/assets/startup.png", 61 | width: newWidth, 62 | height: newHeight, 63 | ), 64 | ), 65 | ); 66 | }, 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/screens/rank_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../src/rust/api/api.dart' as api; 3 | import '../src/rust/udto.dart'; 4 | import 'package:kobi/screens/components/comic_pager.dart'; 5 | import 'package:kobi/screens/recommends_screen.dart'; 6 | import 'components/comic_card.dart'; 7 | 8 | class RankScreen extends StatefulWidget { 9 | const RankScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | _RankScreenState createState() => _RankScreenState(); 13 | } 14 | 15 | class _RankScreenState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | ThemeData theme = Theme.of(context); 19 | return DefaultTabController( 20 | length: 5, 21 | child: Column( 22 | children: [ 23 | SafeArea( 24 | child: Container(), 25 | bottom: false, 26 | ), 27 | Container( 28 | height: 40, 29 | color: theme.colorScheme.secondary.withOpacity(.025), 30 | child: const TabBar( 31 | tabs: [ 32 | Tab(text: '推荐'), 33 | Tab(text: '天'), 34 | Tab(text: '周'), 35 | Tab(text: '月'), 36 | Tab(text: '总'), 37 | ], 38 | ), 39 | ), 40 | const Expanded( 41 | child: TabBarView( 42 | children: [ 43 | RecommendsScreen(), 44 | RankTypeScreen(dateType: "day"), 45 | RankTypeScreen(dateType: "week"), 46 | RankTypeScreen(dateType: "month"), 47 | RankTypeScreen(dateType: "total"), 48 | ], 49 | ), 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | class RankTypeScreen extends StatelessWidget { 58 | final String dateType; 59 | 60 | const RankTypeScreen({Key? key, required this.dateType}) : super(key: key); 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return ComicPager(fetcher: (offset, limit) async { 65 | final result = 66 | await api.rank(dateType: dateType, offset: offset, limit: limit); 67 | return CommonPage( 68 | list: result.list 69 | .map((e) => CommonComicInfo( 70 | author: e.comic.author, 71 | cover: e.comic.cover, 72 | imgType: e.comic.imgType, 73 | name: e.comic.name, 74 | pathWord: e.comic.pathWord, 75 | popular: e.comic.popular, 76 | males: e.comic.males, 77 | females: e.comic.females, 78 | )) 79 | .toList(), 80 | total: result.total, 81 | limit: result.limit, 82 | offset: result.offset, 83 | ); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/screens/recommends_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../src/rust/api/api.dart' as api; 4 | import '../src/rust/udto.dart'; 5 | import 'components/comic_card.dart'; 6 | import 'components/comic_pager.dart'; 7 | 8 | class RecommendsScreen extends StatelessWidget { 9 | const RecommendsScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final pager = ComicPager(fetcher: (offset, limit) async { 14 | final result = await api.recommends(offset: offset, limit: limit); 15 | return CommonPage( 16 | list: result.list 17 | .map((e) => CommonComicInfo( 18 | author: e.author, 19 | cover: e.cover, 20 | imgType: e.imgType, 21 | name: e.name, 22 | pathWord: e.pathWord, 23 | popular: e.popular, 24 | females: e.females, 25 | males: e.males, 26 | )) 27 | .toList(), 28 | total: result.total, 29 | limit: result.limit, 30 | offset: result.offset, 31 | ); 32 | }); 33 | return pager; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/screens/settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:kobi/configs/api_host.dart'; 5 | import 'package:kobi/configs/app_orientation.dart'; 6 | import 'package:kobi/configs/app_theme.dart'; 7 | import 'package:kobi/configs/chapter_order_newest.dart'; 8 | import 'package:kobi/configs/collect_ordering.dart'; 9 | import 'package:kobi/configs/comic_grid_columns.dart'; 10 | import 'package:kobi/configs/comic_pager_type.dart'; 11 | import 'package:kobi/configs/list_volume.dart'; 12 | import 'package:kobi/configs/no_pager_animation.dart'; 13 | 14 | import '../configs/cache_time.dart'; 15 | import '../configs/proxy.dart'; 16 | 17 | class SettingsScreen extends StatelessWidget { 18 | const SettingsScreen({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: AppBar(), 24 | body: ListView( 25 | children: [ 26 | appThemeSetting(context), 27 | apiHostSetting(), 28 | proxySetting(), 29 | cacheTimeNameSetting(), 30 | appOrientationWidget(), 31 | noPagerAnimationSwitch(), 32 | comicPagerTypeSetting(context), 33 | comicGridColumnsSetting(context), 34 | chapterOrderNewestSwitch(), 35 | if (Platform.isAndroid) listVolumeSwitch(), 36 | ], 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/rust/api/simple.dart: -------------------------------------------------------------------------------- 1 | // This file is automatically generated, so please do not edit it. 2 | // @generated by `flutter_rust_bridge`@ 2.9.0. 3 | 4 | // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import 5 | 6 | import '../frb_generated.dart'; 7 | import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; 8 | 9 | String greet({required String name}) => 10 | RustLib.instance.api.crateApiSimpleGreet(name: name); 11 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/appimage/AppRun.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Terminal=false 5 | Name=kobi 6 | Exec=AppRun %u 7 | Icon=AppRun 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /linux/appimage/AppRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/linux/appimage/AppRun.png -------------------------------------------------------------------------------- /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 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 14 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /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 | url_launcher_linux 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | rust_lib_kobi 11 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 20 | endforeach(plugin) 21 | 22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /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_picker 9 | import url_launcher_macos 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 13 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /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/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - rust_lib_kobi (0.0.1): 4 | - FlutterMacOS 5 | - url_launcher_macos (0.0.1): 6 | - FlutterMacOS 7 | 8 | DEPENDENCIES: 9 | - FlutterMacOS (from `Flutter/ephemeral`) 10 | - rust_lib_kobi (from `Flutter/ephemeral/.symlinks/plugins/rust_lib_kobi/macos`) 11 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 12 | 13 | EXTERNAL SOURCES: 14 | FlutterMacOS: 15 | :path: Flutter/ephemeral 16 | rust_lib_kobi: 17 | :path: Flutter/ephemeral/.symlinks/plugins/rust_lib_kobi/macos 18 | url_launcher_macos: 19 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 20 | 21 | SPEC CHECKSUMS: 22 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 23 | rust_lib_kobi: 90bc2fa4f2021821ee631492f76d70b002309cec 24 | url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 25 | 26 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 27 | 28 | COCOAPODS: 1.15.2 29 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | @main 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 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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 = kobi 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = opensource.kobi 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 opensource. 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 Cocoa 2 | import FlutterMacOS 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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: kobi 2 | description: A comic reader. 3 | publish_to: 'none' 4 | 5 | version: 0.0.19+1 6 | 7 | environment: 8 | sdk: ^3.3.4 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | cupertino_icons: ^1.0.8 15 | rust_lib_kobi: 16 | path: rust_builder 17 | flutter_rust_bridge: 2.9.0 18 | 19 | date_format: ^2.0.5 20 | uuid: ^4.1.0 21 | awesome_select: ^5.2.0 22 | another_xlider: ^1.0.1+2 23 | event: ^2.1.2 24 | modal_bottom_sheet: ^3.0.0-pre 25 | photo_view: ^0.14.0 26 | scrollable_positioned_list: ^0.2.3 27 | flutter_styled_toast: ^2.1.3 28 | flutter_svg: ^1.0.3 29 | flutter_html: 3.0.0-alpha.3 30 | flutter_colorpicker: ^1.0.3 31 | crypto: ^3.0.2 32 | ffi: 2.1.4 33 | pull_to_refresh: ^2.0.0 34 | url_launcher: ^6.3.1 35 | permission_handler: ^11.4.0 36 | file_picker: ^10.1.9 37 | 38 | dev_dependencies: 39 | flutter_test: 40 | sdk: flutter 41 | flutter_lints: ^4.0.0 42 | integration_test: 43 | sdk: flutter 44 | 45 | flutter: 46 | uses-material-design: true 47 | assets: 48 | - lib/assets/ 49 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust_lib_kobi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "staticlib"] 8 | 9 | [dependencies] 10 | flutter_rust_bridge = "=2.9.0" 11 | anyhow = "1.0.81" 12 | async_once = "0.2.6" 13 | base64 = "0.22.0" 14 | bytes = "1.6.0" 15 | chrono = { version = "0.4.37", features = ["serde"] } 16 | futures-util = "0.3.30" 17 | hex = "0.4.3" 18 | image = { version = "0.25.1", features = ["jpeg", "gif", "webp", "bmp", "png"] } 19 | itertools = "0.12.1" 20 | lazy_static = "1.4.0" 21 | libc = "0.2.153" 22 | md5 = "0.7.0" 23 | num-iter = "0.1.44" 24 | once_cell = "1.19.0" 25 | prost = "0.12.3" 26 | prost-types = "0.12.3" 27 | regex = "1.10.4" 28 | rsa = "0.9.6" 29 | serde = "1.0.197" 30 | serde_derive = "1.0.197" 31 | serde_json = "1.0.115" 32 | serde_path_to_error = "0.1.16" 33 | tokio = { version = "1.37.0", features = ["full"] } 34 | reqwest = { version = "0.12.2", features = ["rustls-tls", "socks"], default-features = false } 35 | sea-orm = { version = "0.12.15", features = ["sqlx-sqlite", "macros", "runtime-tokio-rustls"], default-features = false } 36 | linked-hash-map = { version = "0.5.6", features = ["serde", "serde_impl"] } 37 | url = "2.5.0" 38 | tracing-subscriber = "0.3.18" 39 | tracing = "0.1.40" 40 | rand = "0.8.5" 41 | async_zip = { version = "0.0.16", features = ["full", "tokio-util", "tokio", "tokio-fs", "async-compression"] } 42 | async-trait = "0.1.79" 43 | -------------------------------------------------------------------------------- /rust/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Do not put code in `mod.rs`, but put in e.g. `simple.rs`. 3 | // 4 | 5 | pub mod api; 6 | pub mod simple; 7 | -------------------------------------------------------------------------------- /rust/src/api/simple.rs: -------------------------------------------------------------------------------- 1 | #[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo 2 | pub fn greet(name: String) -> String { 3 | format!("Hello, {name}!") 4 | } 5 | 6 | #[flutter_rust_bridge::frb(init)] 7 | pub fn init_app() { 8 | // Default utilities - feel free to customize 9 | flutter_rust_bridge::setup_default_user_utils(); 10 | } 11 | -------------------------------------------------------------------------------- /rust/src/copy_client/lib.rs: -------------------------------------------------------------------------------- 1 | pub use super::types::*; 2 | 3 | pub struct Client { 4 | 5 | } 6 | 7 | impl Client { 8 | 9 | async fn request serde::Deserialize<'de>>( 10 | &self, 11 | method: reqwest::Method, 12 | path: &str, 13 | params: serde_json::Value, 14 | ) -> Result { 15 | let mut obj = query.as_object()?; 16 | Ok(serde_json::from_str("")?) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rust/src/copy_client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod dtos; 3 | pub mod types; 4 | 5 | pub use client::*; 6 | pub use dtos::*; 7 | #[allow(unused_imports)] 8 | pub use types::*; 9 | 10 | #[cfg(test)] 11 | mod tests; 12 | -------------------------------------------------------------------------------- /rust/src/copy_client/tests.rs: -------------------------------------------------------------------------------- 1 | use super::client::Client; 2 | use anyhow::Result; 3 | use base64::Engine; 4 | use serde_json::json; 5 | 6 | const API_URL: &str = "aHR0cHM6Ly9hcGkuY29weW1hbmdhLm5ldA=="; 7 | 8 | fn api_url() -> String { 9 | String::from_utf8(base64::prelude::BASE64_STANDARD.decode(API_URL).unwrap()).unwrap() 10 | } 11 | 12 | fn client() -> Client { 13 | Client::new(reqwest::Client::builder().build().unwrap(), api_url()) 14 | } 15 | 16 | #[tokio::test] 17 | async fn test_request() -> Result<()> { 18 | let value = client() 19 | .request( 20 | reqwest::Method::GET, 21 | "/api/v3/comics", 22 | json!({ 23 | "_update": true, 24 | "limit": 21, 25 | "offset": 42, 26 | "platform": 3, 27 | }), 28 | ) 29 | .await?; 30 | println!("{}", serde_json::to_string(&value).unwrap()); 31 | Ok(()) 32 | } 33 | 34 | #[tokio::test] 35 | async fn test_comic() -> Result<()> { 36 | let value = client().comic("dokunidakareteoboreteitai").await?; 37 | println!("{}", serde_json::to_string(&value).unwrap()); 38 | Ok(()) 39 | } 40 | 41 | #[tokio::test] 42 | async fn test_chapters() -> Result<()> { 43 | let value = client() 44 | .comic_chapter("fxzhanshijiuliumei", "default", 100, 0) 45 | .await?; 46 | println!("{}", serde_json::to_string(&value).unwrap()); 47 | Ok(()) 48 | } 49 | 50 | #[tokio::test] 51 | async fn test_recommends() -> Result<()> { 52 | let value = client().recommends(0, 21).await?; 53 | println!("{}", serde_json::to_string(&value).unwrap()); 54 | Ok(()) 55 | } 56 | 57 | #[tokio::test] 58 | async fn test_explore() -> Result<()> { 59 | let value = client() 60 | .explore(Some("-datetime_updated"), None, None, 0, 21) 61 | .await?; 62 | println!("{}", serde_json::to_string(&value).unwrap()); 63 | Ok(()) 64 | } 65 | 66 | #[tokio::test] 67 | async fn test_collect() -> Result<()> { 68 | let client = client(); 69 | client.set_token("token").await; 70 | let value = client 71 | .collect("9581bff2-3892-11ec-8e8b-024352452ce0", true) 72 | .await?; 73 | println!("{}", serde_json::to_string(&value).unwrap()); 74 | Ok(()) 75 | } 76 | 77 | #[tokio::test] 78 | async fn test_collected_comics() -> Result<()> { 79 | let client = client(); 80 | client.set_token("token").await; 81 | let value = client 82 | .collected_comics(1, "-datetime_modifier", 0, 21) 83 | .await?; 84 | println!("{}", serde_json::to_string(&value).unwrap()); 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /rust/src/copy_client/types.rs: -------------------------------------------------------------------------------- 1 | use std::backtrace::Backtrace; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Debug)] 7 | pub struct Error { 8 | pub backtrace: Backtrace, 9 | pub info: ErrorInfo, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum ErrorInfo { 14 | Network(reqwest::Error), 15 | Message(String), 16 | Convert(serde_json::Error), 17 | Other(Box), 18 | } 19 | 20 | impl std::error::Error for Error {} 21 | 22 | impl Display for Error { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 24 | let mut builder = f.debug_struct("copy_client::Error"); 25 | match &self.info { 26 | ErrorInfo::Convert(err) => { 27 | builder.field("kind", &"Convert"); 28 | builder.field("source", err); 29 | } 30 | ErrorInfo::Network(err) => { 31 | builder.field("kind", &"Network"); 32 | builder.field("source", err); 33 | } 34 | ErrorInfo::Message(err) => { 35 | builder.field("kind", &"Message"); 36 | builder.field("source", err); 37 | } 38 | ErrorInfo::Other(err) => { 39 | builder.field("kind", &"Other"); 40 | builder.field("source", err); 41 | } 42 | } 43 | builder.finish() 44 | } 45 | } 46 | 47 | impl Error { 48 | pub(crate) fn message(content: impl Into) -> Self { 49 | Self { 50 | backtrace: Backtrace::capture(), 51 | info: ErrorInfo::Message(content.into()), 52 | } 53 | } 54 | } 55 | 56 | macro_rules! from_error { 57 | ($error_type:ty, $info_type:path) => { 58 | impl From<$error_type> for Error { 59 | fn from(value: $error_type) -> Self { 60 | Self { 61 | backtrace: Backtrace::capture(), 62 | info: $info_type(value), 63 | } 64 | } 65 | } 66 | }; 67 | } 68 | 69 | from_error!(::reqwest::Error, ErrorInfo::Network); 70 | from_error!(::serde_json::Error, ErrorInfo::Convert); 71 | -------------------------------------------------------------------------------- /rust/src/database/active/local_collect.rs: -------------------------------------------------------------------------------- 1 | use crate::database::active::ACTIVE_DATABASE; 2 | use crate::database::create_table_if_not_exists; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::EntityTrait; 5 | use serde_derive::{Deserialize, Serialize}; 6 | use std::convert::TryInto; 7 | use std::ops::Deref; 8 | 9 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] 10 | #[sea_orm(table_name = "local_collect")] 11 | pub struct Model { 12 | #[sea_orm(primary_key, auto_increment = false)] 13 | pub path_word: String, 14 | pub alias: Option, 15 | pub author: String, 16 | pub b_404: bool, 17 | pub b_hidden: bool, 18 | pub ban: i64, 19 | pub brief: String, 20 | pub close_comment: bool, 21 | pub close_roast: bool, 22 | pub cover: String, 23 | pub datetime_updated: String, 24 | pub females: String, 25 | pub free_type: String, 26 | pub img_type: i64, 27 | pub males: String, 28 | pub name: String, 29 | pub popular: i64, 30 | pub reclass: String, 31 | pub region: String, 32 | pub restrict: String, 33 | pub seo_baidu: String, 34 | pub status: String, 35 | pub theme: String, 36 | pub uuid: String, 37 | // 38 | pub append_time: i64, 39 | } 40 | 41 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 42 | pub enum Relation {} 43 | 44 | impl ActiveModelBehavior for ActiveModel {} 45 | 46 | pub(crate) async fn init() { 47 | let db = ACTIVE_DATABASE.get().unwrap().lock().await; 48 | create_table_if_not_exists(db.deref(), Entity).await; 49 | } 50 | -------------------------------------------------------------------------------- /rust/src/database/active/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::connect_db; 2 | use once_cell::sync::OnceCell; 3 | use sea_orm::DatabaseConnection; 4 | use tokio::sync::Mutex; 5 | pub(crate) mod comic_view_log; 6 | pub(crate) mod local_collect; 7 | 8 | pub(crate) static ACTIVE_DATABASE: OnceCell> = OnceCell::new(); 9 | pub(crate) async fn init() { 10 | let db = connect_db("active.db").await; 11 | ACTIVE_DATABASE.set(Mutex::new(db)).unwrap(); 12 | // init tables 13 | comic_view_log::init().await; 14 | local_collect::init().await; 15 | } 16 | -------------------------------------------------------------------------------- /rust/src/database/cache/image_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::database::cache::CACHE_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::sea_query::Expr; 5 | use sea_orm::EntityTrait; 6 | use sea_orm::IntoActiveModel; 7 | use sea_orm::QueryOrder; 8 | use sea_orm::QuerySelect; 9 | use std::ops::Deref; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 12 | #[sea_orm(table_name = "image_cache")] 13 | pub struct Model { 14 | #[sea_orm(primary_key, auto_increment = false)] 15 | pub cache_key: String, 16 | pub cache_time: i64, 17 | pub url: String, 18 | pub useful: String, 19 | pub extends_field_first: Option, 20 | pub extends_field_second: Option, 21 | pub extends_field_third: Option, 22 | pub local_path: String, 23 | pub image_format: String, 24 | pub image_width: u32, 25 | pub image_height: u32, 26 | } 27 | 28 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 29 | pub enum Relation {} 30 | 31 | impl ActiveModelBehavior for ActiveModel {} 32 | 33 | pub(crate) async fn init() { 34 | let gdb = CACHE_DATABASE.get().unwrap().lock().await; 35 | let db = gdb.deref(); 36 | create_table_if_not_exists(db, Entity).await; 37 | if !index_exists(db, "image_cache", "image_cache_idx_cache_time").await { 38 | create_index( 39 | db, 40 | "image_cache", 41 | vec!["cache_time"], 42 | "image_cache_idx_cache_time", 43 | ) 44 | .await; 45 | } 46 | } 47 | 48 | pub(crate) async fn load_image_by_cache_key(cache_key: &str) -> anyhow::Result> { 49 | Ok(Entity::find_by_id(cache_key) 50 | .one(CACHE_DATABASE.get().unwrap().lock().await.deref()) 51 | .await?) 52 | } 53 | 54 | pub(crate) async fn insert(model: Model) -> anyhow::Result { 55 | Ok(model 56 | .into_active_model() 57 | .insert(CACHE_DATABASE.get().unwrap().lock().await.deref()) 58 | .await?) 59 | } 60 | 61 | pub(crate) async fn update_cache_time(cache_key: &str) -> anyhow::Result<()> { 62 | Entity::update_many() 63 | .col_expr( 64 | Column::CacheTime, 65 | Expr::value(chrono::Local::now().timestamp_millis()), 66 | ) 67 | .filter(Column::CacheKey.eq(cache_key)) 68 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 69 | .await?; 70 | Ok(()) 71 | } 72 | 73 | pub(crate) async fn take_100_cache(time: i64) -> anyhow::Result> { 74 | Ok(Entity::find() 75 | .filter(Column::CacheTime.lt(time)) 76 | .order_by_asc(Column::CacheTime) 77 | .limit(100) 78 | .all(CACHE_DATABASE.get().unwrap().lock().await.deref()) 79 | .await?) 80 | } 81 | 82 | pub(crate) async fn delete_by_cache_key(cache_key: String) -> anyhow::Result<()> { 83 | Entity::delete_many() 84 | .filter(Column::CacheKey.eq(cache_key)) 85 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 86 | .await?; 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /rust/src/database/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::connect_db; 2 | use once_cell::sync::OnceCell; 3 | use sea_orm::{ConnectionTrait, DatabaseConnection, ExecResult, Statement}; 4 | use tokio::sync::Mutex; 5 | 6 | pub(crate) mod image_cache; 7 | pub(crate) mod web_cache; 8 | 9 | pub(crate) static CACHE_DATABASE: OnceCell> = OnceCell::new(); 10 | 11 | pub(crate) async fn init() { 12 | let db = connect_db("cache.db").await; 13 | CACHE_DATABASE.set(Mutex::new(db)).unwrap(); 14 | // init tables 15 | image_cache::init().await; 16 | web_cache::init().await; 17 | } 18 | 19 | pub(crate) async fn vacuum() -> anyhow::Result<()> { 20 | let db = CACHE_DATABASE.get().unwrap().lock().await; 21 | let backend = db.get_database_backend(); 22 | let _: ExecResult = db 23 | .execute(Statement::from_string(backend, "VACUUM".to_owned())) 24 | .await?; 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /rust/src/database/download/download_comic_group.rs: -------------------------------------------------------------------------------- 1 | use crate::database::download::DOWNLOAD_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::sea_query::OnConflict; 5 | use sea_orm::{DeleteResult, Order, QueryOrder}; 6 | use sea_orm::{IntoActiveModel}; 7 | use serde_derive::{Deserialize, Serialize}; 8 | use std::ops::Deref; 9 | 10 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] 11 | #[sea_orm(table_name = "download_comic_group")] 12 | pub struct Model { 13 | #[sea_orm(primary_key, auto_increment = false)] 14 | pub comic_path_word: String, 15 | #[sea_orm(primary_key, auto_increment = false)] 16 | pub group_path_word: String, 17 | pub count: i64, 18 | pub name: String, 19 | // 20 | pub group_rank: i64, 21 | } 22 | 23 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 24 | pub enum Relation {} 25 | 26 | impl ActiveModelBehavior for ActiveModel {} 27 | 28 | pub(crate) async fn init() { 29 | let db = DOWNLOAD_DATABASE.get().unwrap().lock().await; 30 | create_table_if_not_exists(db.deref(), Entity).await; 31 | if !index_exists( 32 | db.deref(), 33 | "download_comic_group", 34 | "download_comic_group_idx_comic_path_word", 35 | ) 36 | .await 37 | { 38 | create_index( 39 | db.deref(), 40 | "download_comic_group", 41 | vec!["comic_path_word"], 42 | "download_comic_group_idx_comic_path_word", 43 | ) 44 | .await; 45 | } 46 | } 47 | 48 | pub(crate) async fn delete_by_comic_path_word( 49 | db: &impl ConnectionTrait, 50 | comic_path_word: &str, 51 | ) -> Result { 52 | Entity::delete_many() 53 | .filter(Column::ComicPathWord.eq(comic_path_word)) 54 | .exec(db) 55 | .await 56 | } 57 | 58 | pub(crate) async fn insert_or_update_info( 59 | db: &impl ConnectionTrait, 60 | model: Model, 61 | ) -> Result<(), DbErr> { 62 | // https://www.sea-ql.org/SeaORM/docs/basic-crud/insert/ 63 | // Performing an upsert statement without inserting or updating any of the row will result in a DbErr::RecordNotInserted error. 64 | // If you want RecordNotInserted to be an Ok instead of an error, call .do_nothing(): 65 | Entity::insert(model.into_active_model()) 66 | .on_conflict( 67 | OnConflict::columns(vec![Column::ComicPathWord, Column::GroupPathWord]) 68 | .do_nothing() 69 | .to_owned(), 70 | ) 71 | .exec(db) 72 | .await?; 73 | Ok(()) 74 | } 75 | 76 | // find_by_comic_path_word order by rank 77 | pub(crate) async fn find_by_comic_path_word(comic_path_word: &str) -> anyhow::Result> { 78 | let db = DOWNLOAD_DATABASE.get().unwrap().lock().await; 79 | let result = Entity::find() 80 | .filter(Column::ComicPathWord.eq(comic_path_word)) 81 | .order_by(Column::GroupRank, Order::Asc) 82 | .all(db.deref()) 83 | .await?; 84 | Ok(result) 85 | } 86 | -------------------------------------------------------------------------------- /rust/src/database/properties/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::connect_db; 2 | use once_cell::sync::OnceCell; 3 | use sea_orm::DatabaseConnection; 4 | use tokio::sync::Mutex; 5 | 6 | pub(crate) mod property; 7 | 8 | pub(crate) static PROPERTIES_DATABASE: OnceCell> = OnceCell::new(); 9 | 10 | pub(crate) async fn init() { 11 | let db = connect_db("properties.db").await; 12 | PROPERTIES_DATABASE.set(Mutex::new(db)).unwrap(); 13 | // init tables 14 | property::init().await; 15 | } 16 | -------------------------------------------------------------------------------- /rust/src/database/properties/property.rs: -------------------------------------------------------------------------------- 1 | use crate::database::properties::PROPERTIES_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::IntoActiveModel; 5 | use sea_orm::Set; 6 | use std::ops::Deref; 7 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 8 | #[sea_orm(table_name = "property")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub k: String, 12 | pub v: String, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation {} 17 | 18 | impl ActiveModelBehavior for ActiveModel {} 19 | 20 | pub(crate) async fn init() { 21 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 22 | create_table_if_not_exists(db.deref(), Entity).await; 23 | if !index_exists(db.deref(), "property", "property_idx_k").await { 24 | create_index(db.deref(), "property", vec!["k"], "property_idx_k").await; 25 | } 26 | } 27 | 28 | pub async fn save_property(k: String, v: String) -> anyhow::Result<()> { 29 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 30 | if let Some(in_db) = Entity::find_by_id(k.clone()).one(db.deref()).await? { 31 | let mut in_db = in_db.into_active_model(); 32 | in_db.v = Set(v); 33 | in_db.update(db.deref()).await?; 34 | } else { 35 | Model { k, v } 36 | .into_active_model() 37 | .insert(db.deref()) 38 | .await?; 39 | } 40 | Ok(()) 41 | } 42 | 43 | pub async fn load_property(k: String) -> anyhow::Result { 44 | let in_db = Entity::find_by_id(k) 45 | .one(PROPERTIES_DATABASE.get().unwrap().lock().await.deref()) 46 | .await?; 47 | Ok(if let Some(in_db) = in_db { 48 | in_db.v 49 | } else { 50 | "".to_owned() 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /rust/src/utils.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::collections::hash_map::DefaultHasher; 3 | use std::hash::Hasher; 4 | use std::path::{Path, PathBuf}; 5 | use tokio::sync::{Mutex, MutexGuard}; 6 | 7 | #[allow(dead_code)] 8 | pub(crate) fn join_paths>(paths: Vec

) -> String { 9 | match paths.len() { 10 | 0 => String::default(), 11 | _ => { 12 | let mut path: PathBuf = PathBuf::new(); 13 | for x in paths { 14 | path = path.join(x); 15 | } 16 | return path.to_str().unwrap().to_string(); 17 | } 18 | } 19 | } 20 | 21 | pub(crate) fn create_dir_if_not_exists(path: &str) { 22 | if !Path::new(path).exists() { 23 | std::fs::create_dir_all(path).unwrap(); 24 | } 25 | } 26 | 27 | lazy_static! { 28 | static ref HASH_LOCK: Vec> = { 29 | let mut mutex_vec: Vec> = vec![]; 30 | for _ in 0..256 { 31 | mutex_vec.push(Mutex::<()>::new(())); 32 | } 33 | mutex_vec 34 | }; 35 | } 36 | 37 | pub(crate) async fn hash_lock(url: &String) -> MutexGuard<'static, ()> { 38 | let mut s = DefaultHasher::new(); 39 | s.write(url.as_bytes()); 40 | HASH_LOCK[s.finish() as usize % HASH_LOCK.len()] 41 | .lock() 42 | .await 43 | } 44 | 45 | pub(crate) fn allowed_file_name(title: &str) -> String { 46 | title 47 | .replace("#", "_") 48 | .replace("'", "_") 49 | .replace("/", "_") 50 | .replace("\\", "_") 51 | .replace(":", "_") 52 | .replace("*", "_") 53 | .replace("?", "_") 54 | .replace("\"", "_") 55 | .replace(">", "_") 56 | .replace("<", "_") 57 | .replace("|", "_") 58 | .replace("&", "_") 59 | } 60 | 61 | -------------------------------------------------------------------------------- /rust_builder/.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 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | build/ 30 | -------------------------------------------------------------------------------- /rust_builder/README.md: -------------------------------------------------------------------------------- 1 | Please ignore this folder, which is just glue to build Rust with Flutter. -------------------------------------------------------------------------------- /rust_builder/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /rust_builder/android/build.gradle: -------------------------------------------------------------------------------- 1 | // The Android Gradle Plugin builds the native code with the Android NDK. 2 | 3 | group 'com.flutter_rust_bridge.rust_lib_kobi' 4 | version '1.0' 5 | 6 | buildscript { 7 | repositories { 8 | google() 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | // The Android Gradle Plugin knows how to build native code with the NDK. 14 | classpath 'com.android.tools.build:gradle:7.3.0' 15 | } 16 | } 17 | 18 | rootProject.allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | apply plugin: 'com.android.library' 26 | 27 | android { 28 | if (project.android.hasProperty("namespace")) { 29 | namespace 'com.flutter_rust_bridge.rust_lib_kobi' 30 | } 31 | 32 | // Bumping the plugin compileSdkVersion requires all clients of this plugin 33 | // to bump the version in their app. 34 | compileSdkVersion 33 35 | 36 | // Use the NDK version 37 | // declared in /android/app/build.gradle file of the Flutter project. 38 | // Replace it with a version number if this plugin requires a specfic NDK version. 39 | // (e.g. ndkVersion "23.1.7779620") 40 | ndkVersion android.ndkVersion 41 | 42 | compileOptions { 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | 47 | defaultConfig { 48 | minSdkVersion 19 49 | } 50 | } 51 | 52 | apply from: "../cargokit/gradle/plugin.gradle" 53 | cargokit { 54 | manifestDir = "../../rust" 55 | libname = "rust_lib_kobi" 56 | } 57 | -------------------------------------------------------------------------------- /rust_builder/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rust_lib_kobi' 2 | -------------------------------------------------------------------------------- /rust_builder/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /rust_builder/cargokit/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .dart_tool 3 | *.iml 4 | !pubspec.lock 5 | -------------------------------------------------------------------------------- /rust_builder/cargokit/LICENSE: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | Copyright 2022 Matej Knopp 5 | 6 | ================================================================================ 7 | 8 | MIT LICENSE 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 14 | of the Software, and to permit persons to whom the Software is furnished to do 15 | so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 23 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | ================================================================================ 28 | 29 | APACHE LICENSE, VERSION 2.0 30 | 31 | Licensed under the Apache License, Version 2.0 (the "License"); 32 | you may not use this file except in compliance with the License. 33 | You may obtain a copy of the License at 34 | 35 | http://www.apache.org/licenses/LICENSE-2.0 36 | 37 | Unless required by applicable law or agreed to in writing, software 38 | distributed under the License is distributed on an "AS IS" BASIS, 39 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 40 | See the License for the specific language governing permissions and 41 | limitations under the License. 42 | 43 | -------------------------------------------------------------------------------- /rust_builder/cargokit/README: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | Experimental repository to provide glue for seamlessly integrating cargo build 5 | with flutter plugins and packages. 6 | 7 | See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ 8 | for a tutorial on how to use Cargokit. 9 | 10 | Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. 11 | 12 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_pod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | BASEDIR=$(dirname "$0") 5 | 6 | # Workaround for https://github.com/dart-lang/pub/issues/4010 7 | BASEDIR=$(cd "$BASEDIR" ; pwd -P) 8 | 9 | # Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project 10 | NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` 11 | 12 | export PATH=${NEW_PATH%?} # remove trailing : 13 | 14 | env 15 | 16 | # Platform name (macosx, iphoneos, iphonesimulator) 17 | export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME 18 | 19 | # Arctive architectures (arm64, armv7, x86_64), space separated. 20 | export CARGOKIT_DARWIN_ARCHS=$ARCHS 21 | 22 | # Current build configuration (Debug, Release) 23 | export CARGOKIT_CONFIGURATION=$CONFIGURATION 24 | 25 | # Path to directory containing Cargo.toml. 26 | export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 27 | 28 | # Temporary directory for build artifacts. 29 | export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR 30 | 31 | # Output directory for final artifacts. 32 | export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME 33 | 34 | # Directory to store built tool artifacts. 35 | export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool 36 | 37 | # Directory inside root project. Not necessarily the top level directory of root project. 38 | export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT 39 | 40 | FLUTTER_EXPORT_BUILD_ENVIRONMENT=( 41 | "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS 42 | "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS 43 | ) 44 | 45 | for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" 46 | do 47 | if [[ -f "$path" ]]; then 48 | source "$path" 49 | fi 50 | done 51 | 52 | sh "$BASEDIR/run_build_tool.sh" build-pod "$@" 53 | 54 | # Make a symlink from built framework to phony file, which will be used as input to 55 | # build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate 56 | # attribute on custom build phase) 57 | ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" 58 | ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" 59 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/README.md: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | A sample command-line application with an entrypoint in `bin/`, library code 5 | in `lib/`, and example unit test in `test/`. 6 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This is copied from Cargokit (which is the official way to use it currently) 2 | # Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | # This file configures the static analysis results for your project (errors, 5 | # warnings, and lints). 6 | # 7 | # This enables the 'recommended' set of lints from `package:lints`. 8 | # This set helps identify many issues that may lead to problems when running 9 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 10 | # style and format. 11 | # 12 | # If you want a smaller set of lints you can change this to specify 13 | # 'package:lints/core.yaml'. These are just the most critical lints 14 | # (the recommended set includes the core lints). 15 | # The core lints are also what is used by pub.dev for scoring packages. 16 | 17 | include: package:lints/recommended.yaml 18 | 19 | # Uncomment the following section to specify additional rules. 20 | 21 | linter: 22 | rules: 23 | - prefer_relative_imports 24 | - directives_ordering 25 | 26 | # analyzer: 27 | # exclude: 28 | # - path/to/excluded/files/** 29 | 30 | # For more information about the core and recommended set of lints, see 31 | # https://dart.dev/go/core-lints 32 | 33 | # For additional information about configuring this file, see 34 | # https://dart.dev/guides/language/analysis-options 35 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/bin/build_tool.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'package:build_tool/build_tool.dart' as build_tool; 5 | 6 | void main(List arguments) { 7 | build_tool.runMain(arguments); 8 | } 9 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/build_tool.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'src/build_tool.dart' as build_tool; 5 | 6 | Future runMain(List args) async { 7 | return build_tool.runMain(args); 8 | } 9 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/build_cmake.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:path/path.dart' as path; 7 | 8 | import 'artifacts_provider.dart'; 9 | import 'builder.dart'; 10 | import 'environment.dart'; 11 | import 'options.dart'; 12 | import 'target.dart'; 13 | 14 | class BuildCMake { 15 | final CargokitUserOptions userOptions; 16 | 17 | BuildCMake({required this.userOptions}); 18 | 19 | Future build() async { 20 | final targetPlatform = Environment.targetPlatform; 21 | final target = Target.forFlutterName(Environment.targetPlatform); 22 | if (target == null) { 23 | throw Exception("Unknown target platform: $targetPlatform"); 24 | } 25 | 26 | final environment = BuildEnvironment.fromEnvironment(isAndroid: false); 27 | final provider = 28 | ArtifactProvider(environment: environment, userOptions: userOptions); 29 | final artifacts = await provider.getArtifacts([target]); 30 | 31 | final libs = artifacts[target]!; 32 | 33 | for (final lib in libs) { 34 | if (lib.type == AritifactType.dylib) { 35 | File(lib.path) 36 | .copySync(path.join(Environment.outputDir, lib.finalFileName)); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/build_gradle.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:logging/logging.dart'; 7 | import 'package:path/path.dart' as path; 8 | 9 | import 'artifacts_provider.dart'; 10 | import 'builder.dart'; 11 | import 'environment.dart'; 12 | import 'options.dart'; 13 | import 'target.dart'; 14 | 15 | final log = Logger('build_gradle'); 16 | 17 | class BuildGradle { 18 | BuildGradle({required this.userOptions}); 19 | 20 | final CargokitUserOptions userOptions; 21 | 22 | Future build() async { 23 | final targets = Environment.targetPlatforms.map((arch) { 24 | final target = Target.forFlutterName(arch); 25 | if (target == null) { 26 | throw Exception( 27 | "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); 28 | } 29 | return target; 30 | }).toList(); 31 | 32 | final environment = BuildEnvironment.fromEnvironment(isAndroid: true); 33 | final provider = 34 | ArtifactProvider(environment: environment, userOptions: userOptions); 35 | final artifacts = await provider.getArtifacts(targets); 36 | 37 | for (final target in targets) { 38 | final libs = artifacts[target]!; 39 | final outputDir = path.join(Environment.outputDir, target.android!); 40 | Directory(outputDir).createSync(recursive: true); 41 | 42 | for (final lib in libs) { 43 | if (lib.type == AritifactType.dylib) { 44 | File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/cargo.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:path/path.dart' as path; 7 | import 'package:toml/toml.dart'; 8 | 9 | class ManifestException { 10 | ManifestException(this.message, {required this.fileName}); 11 | 12 | final String? fileName; 13 | final String message; 14 | 15 | @override 16 | String toString() { 17 | if (fileName != null) { 18 | return 'Failed to parse package manifest at $fileName: $message'; 19 | } else { 20 | return 'Failed to parse package manifest: $message'; 21 | } 22 | } 23 | } 24 | 25 | class CrateInfo { 26 | CrateInfo({required this.packageName}); 27 | 28 | final String packageName; 29 | 30 | static CrateInfo parseManifest(String manifest, {final String? fileName}) { 31 | final toml = TomlDocument.parse(manifest); 32 | final package = toml.toMap()['package']; 33 | if (package == null) { 34 | throw ManifestException('Missing package section', fileName: fileName); 35 | } 36 | final name = package['name']; 37 | if (name == null) { 38 | throw ManifestException('Missing package name', fileName: fileName); 39 | } 40 | return CrateInfo(packageName: name); 41 | } 42 | 43 | static CrateInfo load(String manifestDir) { 44 | final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); 45 | final manifest = manifestFile.readAsStringSync(); 46 | return parseManifest(manifest, fileName: manifestFile.path); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/environment.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | extension on String { 7 | String resolveSymlink() => File(this).resolveSymbolicLinksSync(); 8 | } 9 | 10 | class Environment { 11 | /// Current build configuration (debug or release). 12 | static String get configuration => 13 | _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); 14 | 15 | static bool get isDebug => configuration == 'debug'; 16 | static bool get isRelease => configuration == 'release'; 17 | 18 | /// Temporary directory where Rust build artifacts are placed. 19 | static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); 20 | 21 | /// Final output directory where the build artifacts are placed. 22 | static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); 23 | 24 | /// Path to the crate manifest (containing Cargo.toml). 25 | static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); 26 | 27 | /// Directory inside root project. Not necessarily root folder. Symlinks are 28 | /// not resolved on purpose. 29 | static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); 30 | 31 | // Pod 32 | 33 | /// Platform name (macosx, iphoneos, iphonesimulator). 34 | static String get darwinPlatformName => 35 | _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); 36 | 37 | /// List of architectures to build for (arm64, armv7, x86_64). 38 | static List get darwinArchs => 39 | _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); 40 | 41 | // Gradle 42 | static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); 43 | static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); 44 | static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); 45 | static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); 46 | static List get targetPlatforms => 47 | _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); 48 | 49 | // CMAKE 50 | static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); 51 | 52 | static String _getEnv(String key) { 53 | final res = Platform.environment[key]; 54 | if (res == null) { 55 | throw Exception("Missing environment variable $key"); 56 | } 57 | return res; 58 | } 59 | 60 | static String _getEnvPath(String key) { 61 | final res = _getEnv(key); 62 | if (Directory(res).existsSync()) { 63 | return res.resolveSymlink(); 64 | } else { 65 | return res; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/logging.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:logging/logging.dart'; 7 | 8 | const String kSeparator = "--"; 9 | const String kDoubleSeparator = "=="; 10 | 11 | bool _lastMessageWasSeparator = false; 12 | 13 | void _log(LogRecord rec) { 14 | final prefix = '${rec.level.name}: '; 15 | final out = rec.level == Level.SEVERE ? stderr : stdout; 16 | if (rec.message == kSeparator) { 17 | if (!_lastMessageWasSeparator) { 18 | out.write(prefix); 19 | out.writeln('-' * 80); 20 | _lastMessageWasSeparator = true; 21 | } 22 | return; 23 | } else if (rec.message == kDoubleSeparator) { 24 | out.write(prefix); 25 | out.writeln('=' * 80); 26 | _lastMessageWasSeparator = true; 27 | return; 28 | } 29 | out.write(prefix); 30 | out.writeln(rec.message); 31 | _lastMessageWasSeparator = false; 32 | } 33 | 34 | void initLogging() { 35 | Logger.root.level = Level.INFO; 36 | Logger.root.onRecord.listen((LogRecord rec) { 37 | final lines = rec.message.split('\n'); 38 | for (final line in lines) { 39 | if (line.isNotEmpty || lines.length == 1 || line != lines.last) { 40 | _log(LogRecord( 41 | rec.level, 42 | line, 43 | rec.loggerName, 44 | )); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | void enableVerboseLogging() { 51 | Logger.root.level = Level.ALL; 52 | } 53 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart: -------------------------------------------------------------------------------- 1 | /// This is copied from Cargokit (which is the official way to use it currently) 2 | /// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:ed25519_edwards/ed25519_edwards.dart'; 7 | import 'package:http/http.dart'; 8 | 9 | import 'artifacts_provider.dart'; 10 | import 'cargo.dart'; 11 | import 'crate_hash.dart'; 12 | import 'options.dart'; 13 | import 'precompile_binaries.dart'; 14 | import 'target.dart'; 15 | 16 | class VerifyBinaries { 17 | VerifyBinaries({ 18 | required this.manifestDir, 19 | }); 20 | 21 | final String manifestDir; 22 | 23 | Future run() async { 24 | final crateInfo = CrateInfo.load(manifestDir); 25 | 26 | final config = CargokitCrateOptions.load(manifestDir: manifestDir); 27 | final precompiledBinaries = config.precompiledBinaries; 28 | if (precompiledBinaries == null) { 29 | stdout.writeln('Crate does not support precompiled binaries.'); 30 | } else { 31 | final crateHash = CrateHash.compute(manifestDir); 32 | stdout.writeln('Crate hash: $crateHash'); 33 | 34 | for (final target in Target.all) { 35 | final message = 'Checking ${target.rust}...'; 36 | stdout.write(message.padRight(40)); 37 | stdout.flush(); 38 | 39 | final artifacts = getArtifactNames( 40 | target: target, 41 | libraryName: crateInfo.packageName, 42 | remote: true, 43 | ); 44 | 45 | final prefix = precompiledBinaries.uriPrefix; 46 | 47 | bool ok = true; 48 | 49 | for (final artifact in artifacts) { 50 | final fileName = PrecompileBinaries.fileName(target, artifact); 51 | final signatureFileName = 52 | PrecompileBinaries.signatureFileName(target, artifact); 53 | 54 | final url = Uri.parse('$prefix$crateHash/$fileName'); 55 | final signatureUrl = 56 | Uri.parse('$prefix$crateHash/$signatureFileName'); 57 | 58 | final signature = await get(signatureUrl); 59 | if (signature.statusCode != 200) { 60 | stdout.writeln('MISSING'); 61 | ok = false; 62 | break; 63 | } 64 | final asset = await get(url); 65 | if (asset.statusCode != 200) { 66 | stdout.writeln('MISSING'); 67 | ok = false; 68 | break; 69 | } 70 | 71 | if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, 72 | signature.bodyBytes)) { 73 | stdout.writeln('INVALID SIGNATURE'); 74 | ok = false; 75 | } 76 | } 77 | 78 | if (ok) { 79 | stdout.writeln('OK'); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rust_builder/cargokit/build_tool/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # This is copied from Cargokit (which is the official way to use it currently) 2 | # Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin 3 | 4 | name: build_tool 5 | description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. 6 | publish_to: none 7 | version: 1.0.0 8 | 9 | environment: 10 | sdk: ">=3.0.0 <4.0.0" 11 | 12 | # Add regular dependencies here. 13 | dependencies: 14 | # these are pinned on purpose because the bundle_tool_runner doesn't have 15 | # pubspec.lock. See run_build_tool.sh 16 | logging: 1.2.0 17 | path: 1.8.0 18 | version: 3.0.0 19 | collection: 1.18.0 20 | ed25519_edwards: 0.3.1 21 | hex: 0.2.0 22 | yaml: 3.1.2 23 | source_span: 1.10.0 24 | github: 9.17.0 25 | args: 2.4.2 26 | crypto: 3.0.3 27 | convert: 3.1.1 28 | http: 1.1.0 29 | toml: 0.14.0 30 | 31 | dev_dependencies: 32 | lints: ^2.1.0 33 | test: ^1.24.0 34 | -------------------------------------------------------------------------------- /rust_builder/cargokit/cmake/resolve_symlinks.ps1: -------------------------------------------------------------------------------- 1 | function Resolve-Symlinks { 2 | [CmdletBinding()] 3 | [OutputType([string])] 4 | param( 5 | [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 6 | [string] $Path 7 | ) 8 | 9 | [string] $separator = '/' 10 | [string[]] $parts = $Path.Split($separator) 11 | 12 | [string] $realPath = '' 13 | foreach ($part in $parts) { 14 | if ($realPath -and !$realPath.EndsWith($separator)) { 15 | $realPath += $separator 16 | } 17 | $realPath += $part 18 | $item = Get-Item $realPath 19 | if ($item.Target) { 20 | $realPath = $item.Target.Replace('\', '/') 21 | } 22 | } 23 | $realPath 24 | } 25 | 26 | $path=Resolve-Symlinks -Path $args[0] 27 | Write-Host $path 28 | -------------------------------------------------------------------------------- /rust_builder/cargokit/run_build_tool.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | setlocal ENABLEDELAYEDEXPANSION 5 | 6 | SET BASEDIR=%~dp0 7 | 8 | if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( 9 | mkdir "%CARGOKIT_TOOL_TEMP_DIR%" 10 | ) 11 | cd /D "%CARGOKIT_TOOL_TEMP_DIR%" 12 | 13 | SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool 14 | SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart 15 | 16 | set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% 17 | 18 | ( 19 | echo name: build_tool_runner 20 | echo version: 1.0.0 21 | echo publish_to: none 22 | echo. 23 | echo environment: 24 | echo sdk: '^>=3.0.0 ^<4.0.0' 25 | echo. 26 | echo dependencies: 27 | echo build_tool: 28 | echo path: %BUILD_TOOL_PKG_DIR_POSIX% 29 | ) >pubspec.yaml 30 | 31 | if not exist bin ( 32 | mkdir bin 33 | ) 34 | 35 | ( 36 | echo import 'package:build_tool/build_tool.dart' as build_tool; 37 | echo void main^(List^ args^) ^{ 38 | echo build_tool.runMain^(args^); 39 | echo ^} 40 | ) >bin\build_tool_runner.dart 41 | 42 | SET PRECOMPILED=bin\build_tool_runner.dill 43 | 44 | REM To detect changes in package we compare output of DIR /s (recursive) 45 | set PREV_PACKAGE_INFO=.dart_tool\package_info.prev 46 | set CUR_PACKAGE_INFO=.dart_tool\package_info.cur 47 | 48 | DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" 49 | 50 | REM Last line in dir output is free space on harddrive. That is bound to 51 | REM change between invocation so we need to remove it 52 | ( 53 | Set "Line=" 54 | For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( 55 | SetLocal EnableDelayedExpansion 56 | If Defined Line Echo !Line! 57 | EndLocal 58 | Set "Line=%%A") 59 | ) >"%CUR_PACKAGE_INFO%" 60 | DEL "%CUR_PACKAGE_INFO%_orig" 61 | 62 | REM Compare current directory listing with previous 63 | FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 64 | 65 | If %ERRORLEVEL% neq 0 ( 66 | REM Changed - copy current to previous and remove precompiled kernel 67 | if exist "%PREV_PACKAGE_INFO%" ( 68 | DEL "%PREV_PACKAGE_INFO%" 69 | ) 70 | MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" 71 | if exist "%PRECOMPILED%" ( 72 | DEL "%PRECOMPILED%" 73 | ) 74 | ) 75 | 76 | REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% 77 | REM which means we need to do pub get and precompile 78 | if not exist "%PRECOMPILED%" ( 79 | echo Running pub get in "%cd%" 80 | "%DART%" pub get --no-precompile 81 | "%DART%" compile kernel bin/build_tool_runner.dart 82 | ) 83 | 84 | "%DART%" "%PRECOMPILED%" %* 85 | 86 | REM 253 means invalid snapshot version. 87 | If %ERRORLEVEL% equ 253 ( 88 | "%DART%" pub get --no-precompile 89 | "%DART%" compile kernel bin/build_tool_runner.dart 90 | "%DART%" "%PRECOMPILED%" %* 91 | ) 92 | -------------------------------------------------------------------------------- /rust_builder/cargokit/run_build_tool.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | BASEDIR=$(dirname "$0") 6 | 7 | mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" 8 | 9 | cd "$CARGOKIT_TOOL_TEMP_DIR" 10 | 11 | # Write a very simple bin package in temp folder that depends on build_tool package 12 | # from Cargokit. This is done to ensure that we don't pollute Cargokit folder 13 | # with .dart_tool contents. 14 | 15 | BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" 16 | 17 | if [[ -z $FLUTTER_ROOT ]]; then # not defined 18 | DART=dart 19 | else 20 | DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" 21 | fi 22 | 23 | cat << EOF > "pubspec.yaml" 24 | name: build_tool_runner 25 | version: 1.0.0 26 | publish_to: none 27 | 28 | environment: 29 | sdk: '>=3.0.0 <4.0.0' 30 | 31 | dependencies: 32 | build_tool: 33 | path: "$BUILD_TOOL_PKG_DIR" 34 | EOF 35 | 36 | mkdir -p "bin" 37 | 38 | cat << EOF > "bin/build_tool_runner.dart" 39 | import 'package:build_tool/build_tool.dart' as build_tool; 40 | void main(List args) { 41 | build_tool.runMain(args); 42 | } 43 | EOF 44 | 45 | # Create alias for `shasum` if it does not exist and `sha1sum` exists 46 | if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then 47 | shopt -s expand_aliases 48 | alias shasum="sha1sum" 49 | fi 50 | 51 | # Dart run will not cache any package that has a path dependency, which 52 | # is the case for our build_tool_runner. So instead we precompile the package 53 | # ourselves. 54 | # To invalidate the cached kernel we use the hash of ls -LR of the build_tool 55 | # package directory. This should be good enough, as the build_tool package 56 | # itself is not meant to have any path dependencies. 57 | 58 | if [[ "$OSTYPE" == "darwin"* ]]; then 59 | PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) 60 | else 61 | PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) 62 | fi 63 | 64 | PACKAGE_HASH_FILE=".package_hash" 65 | 66 | if [ -f "$PACKAGE_HASH_FILE" ]; then 67 | EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") 68 | if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then 69 | rm "$PACKAGE_HASH_FILE" 70 | fi 71 | fi 72 | 73 | # Run pub get if needed. 74 | if [ ! -f "$PACKAGE_HASH_FILE" ]; then 75 | "$DART" pub get --no-precompile 76 | "$DART" compile kernel bin/build_tool_runner.dart 77 | echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" 78 | fi 79 | 80 | set +e 81 | 82 | "$DART" bin/build_tool_runner.dill "$@" 83 | 84 | exit_code=$? 85 | 86 | # 253 means invalid snapshot version. 87 | if [ $exit_code == 253 ]; then 88 | "$DART" pub get --no-precompile 89 | "$DART" compile kernel bin/build_tool_runner.dart 90 | "$DART" bin/build_tool_runner.dill "$@" 91 | exit_code=$? 92 | fi 93 | 94 | exit $exit_code 95 | -------------------------------------------------------------------------------- /rust_builder/ios/Classes/dummy_file.c: -------------------------------------------------------------------------------- 1 | // This is an empty file to force CocoaPods to create a framework. 2 | -------------------------------------------------------------------------------- /rust_builder/ios/rust_lib_kobi.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint rust_lib_kobi.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'rust_lib_kobi' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter FFI plugin project.' 9 | s.description = <<-DESC 10 | A new Flutter FFI plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | 16 | # This will ensure the source files in Classes/ are included in the native 17 | # builds of apps using this FFI plugin. Podspec does not support relative 18 | # paths, so Classes contains a forwarder C file that relatively imports 19 | # `../src/*` so that the C sources can be shared among all target platforms. 20 | s.source = { :path => '.' } 21 | s.source_files = 'Classes/**/*' 22 | s.dependency 'Flutter' 23 | s.platform = :ios, '11.0' 24 | 25 | # Flutter.framework does not contain a i386 slice. 26 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 27 | s.swift_version = '5.0' 28 | 29 | s.script_phase = { 30 | :name => 'Build Rust library', 31 | # First argument is relative path to the `rust` folder, second is name of rust library 32 | :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_kobi', 33 | :execution_position => :before_compile, 34 | :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], 35 | # Let XCode know that the static library referenced in -force_load below is 36 | # created by this build step. 37 | :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_kobi.a"], 38 | } 39 | s.pod_target_xcconfig = { 40 | 'DEFINES_MODULE' => 'YES', 41 | # Flutter.framework does not contain a i386 slice. 42 | 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', 43 | 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_kobi.a', 44 | } 45 | end -------------------------------------------------------------------------------- /rust_builder/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The Flutter tooling requires that developers have CMake 3.10 or later 2 | # installed. You should not increase this version, as doing so will cause 3 | # the plugin to fail to compile for some customers of the plugin. 4 | cmake_minimum_required(VERSION 3.10) 5 | 6 | # Project-level configuration. 7 | set(PROJECT_NAME "rust_lib_kobi") 8 | project(${PROJECT_NAME} LANGUAGES CXX) 9 | 10 | include("../cargokit/cmake/cargokit.cmake") 11 | apply_cargokit(${PROJECT_NAME} ../../rust rust_lib_kobi "") 12 | 13 | # List of absolute paths to libraries that should be bundled with the plugin. 14 | # This list could contain prebuilt libraries, or libraries created by an 15 | # external build triggered from this build file. 16 | set(rust_lib_kobi_bundled_libraries 17 | "${${PROJECT_NAME}_cargokit_lib}" 18 | PARENT_SCOPE 19 | ) 20 | -------------------------------------------------------------------------------- /rust_builder/macos/Classes/dummy_file.c: -------------------------------------------------------------------------------- 1 | // This is an empty file to force CocoaPods to create a framework. 2 | -------------------------------------------------------------------------------- /rust_builder/macos/rust_lib_kobi.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint rust_lib_kobi.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'rust_lib_kobi' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter FFI plugin project.' 9 | s.description = <<-DESC 10 | A new Flutter FFI plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | 16 | # This will ensure the source files in Classes/ are included in the native 17 | # builds of apps using this FFI plugin. Podspec does not support relative 18 | # paths, so Classes contains a forwarder C file that relatively imports 19 | # `../src/*` so that the C sources can be shared among all target platforms. 20 | s.source = { :path => '.' } 21 | s.source_files = 'Classes/**/*' 22 | s.dependency 'FlutterMacOS' 23 | 24 | s.platform = :osx, '10.11' 25 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 26 | s.swift_version = '5.0' 27 | 28 | s.script_phase = { 29 | :name => 'Build Rust library', 30 | # First argument is relative path to the `rust` folder, second is name of rust library 31 | :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_kobi', 32 | :execution_position => :before_compile, 33 | :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], 34 | # Let XCode know that the static library referenced in -force_load below is 35 | # created by this build step. 36 | :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_kobi.a"], 37 | } 38 | s.pod_target_xcconfig = { 39 | 'DEFINES_MODULE' => 'YES', 40 | # Flutter.framework does not contain a i386 slice. 41 | 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', 42 | 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_kobi.a', 43 | } 44 | end -------------------------------------------------------------------------------- /rust_builder/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rust_lib_kobi 2 | description: "Utility to build Rust code" 3 | version: 0.0.1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=3.3.0 <4.0.0' 8 | flutter: '>=3.3.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | plugin_platform_interface: ^2.0.2 14 | 15 | dev_dependencies: 16 | ffi: ^2.0.2 17 | ffigen: ^11.0.0 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^2.0.0 21 | 22 | flutter: 23 | plugin: 24 | platforms: 25 | android: 26 | ffiPlugin: true 27 | ios: 28 | ffiPlugin: true 29 | linux: 30 | ffiPlugin: true 31 | macos: 32 | ffiPlugin: true 33 | windows: 34 | ffiPlugin: true 35 | -------------------------------------------------------------------------------- /rust_builder/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ 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 | -------------------------------------------------------------------------------- /rust_builder/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The Flutter tooling requires that developers have a version of Visual Studio 2 | # installed that includes CMake 3.14 or later. You should not increase this 3 | # version, as doing so will cause the plugin to fail to compile for some 4 | # customers of the plugin. 5 | cmake_minimum_required(VERSION 3.14) 6 | 7 | # Project-level configuration. 8 | set(PROJECT_NAME "rust_lib_kobi") 9 | project(${PROJECT_NAME} LANGUAGES CXX) 10 | 11 | include("../cargokit/cmake/cargokit.cmake") 12 | apply_cargokit(${PROJECT_NAME} ../../../../../../rust rust_lib_kobi "") 13 | 14 | # List of absolute paths to libraries that should be bundled with the plugin. 15 | # This list could contain prebuilt libraries, or libraries created by an 16 | # external build triggered from this build file. 17 | set(rust_lib_kobi_bundled_libraries 18 | "${${PROJECT_NAME}_cargokit_lib}" 19 | PARENT_SCOPE 20 | ) 21 | -------------------------------------------------------------------------------- /scripts/thin-payload.sh: -------------------------------------------------------------------------------- 1 | # 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa) 2 | 3 | foreachThin(){ 4 | for file in $1/* 5 | do 6 | if test -f $file 7 | then 8 | mime=$(file --mime-type -b $file) 9 | if [ "$mime" == 'application/x-mach-binary' ] || [ "${file##*.}"x = "dylib"x ] 10 | then 11 | echo thin $file 12 | xcrun -sdk iphoneos lipo "$file" -thin arm64 -output "$file" 13 | xcrun -sdk iphoneos bitcode_strip "$file" -r -o "$file" 14 | strip -S -x "$file" -o "$file" 15 | fi 16 | fi 17 | if test -d $file 18 | then 19 | foreachThin $file 20 | fi 21 | done 22 | } 23 | 24 | foreachThin ./Payload 25 | -------------------------------------------------------------------------------- /test_driver/integration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:integration_test/integration_test_driver.dart'; 2 | 3 | Future main() => integrationDriver(); 4 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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 | kobi 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kobi", 3 | "short_name": "kobi", 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/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 | 12 | void RegisterPlugins(flutter::PluginRegistry* registry) { 13 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 14 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 15 | UrlLauncherWindowsRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 17 | } 18 | -------------------------------------------------------------------------------- /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 | permission_handler_windows 7 | url_launcher_windows 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | rust_lib_kobi 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /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/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"kobi", 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/niuhuan/kobi/3954a3bc1825c4707d79d55711c1080723912042/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 | -------------------------------------------------------------------------------- /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 | unsigned 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 | --------------------------------------------------------------------------------