├── .editorconfig ├── .gitignore ├── .metadata ├── .tool-versions ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── niuhuan │ │ │ │ └── html │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ci ├── Cargo.lock ├── Cargo.toml ├── src │ ├── check_asset │ │ └── main.rs │ ├── check_release │ │ └── main.rs │ ├── main.rs │ └── upload_asset │ │ └── main.rs ├── version.code.txt └── version.info.txt ├── images ├── ic.png └── st01.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── justfile ├── l10n.yaml ├── lib ├── assets │ ├── android.svg │ ├── error.png │ ├── github.svg │ └── init.png ├── commons.dart ├── configs │ ├── android_display_mode.dart │ ├── android_secure_flag.dart │ ├── auto_clean.dart │ ├── auto_full_screen.dart │ ├── content_failed_reload_action.dart │ ├── export_rename.dart │ ├── full_screen_action.dart │ ├── gallery_preload_count.dart │ ├── keyboard_controller.dart │ ├── list_layout.dart │ ├── no_animation.dart │ ├── pager_action.dart │ ├── platform.dart │ ├── proxy.dart │ ├── reader_direction.dart │ ├── reader_slider_position.dart │ ├── reader_type.dart │ ├── themes.dart │ ├── time_offset_hour.dart │ ├── version.dart │ └── volume_controller.dart ├── cross.dart ├── ffi.dart ├── l10n │ ├── app_en.arb │ └── app_zh.arb ├── main.dart └── screens │ ├── about_screen.dart │ ├── comic_reader_screen.dart │ ├── comics_screen.dart │ ├── components │ ├── badge.dart │ ├── comic_info_card.dart │ ├── comic_list.dart │ ├── comic_list_builder.dart │ ├── comic_pager.dart │ ├── comic_tags_card.dart │ ├── content_builder.dart │ ├── content_error.dart │ ├── content_loading.dart │ ├── content_message.dart │ ├── error_types.dart │ ├── fit_button.dart │ ├── gesture_zoom_box.dart │ ├── image_reader.dart │ ├── images.dart │ ├── item_builder.dart │ ├── mouse_and_touch_scroll_behavior.dart │ └── navigator.dart │ ├── file_photo_view_screen.dart │ ├── init_screen.dart │ ├── settings_screen.dart │ └── theme_screen.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 └── rust.cmake ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── 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 ├── native ├── Cargo.toml ├── native.xcodeproj │ ├── project.pbxproj │ └── xcuserdata │ │ ├── niuhuan.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── vdinh.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── src │ ├── api.rs │ ├── database │ ├── active │ │ ├── comic_view_log.rs │ │ └── mod.rs │ ├── cache │ │ ├── image_cache.rs │ │ ├── mod.rs │ │ └── web_cache.rs │ ├── mod.rs │ └── properties │ │ ├── mod.rs │ │ └── property.rs │ ├── hitomi_client │ ├── client.rs │ ├── entities.rs │ ├── gg.rs │ ├── mod.rs │ └── tests.rs │ ├── lib.rs │ ├── local.rs │ ├── tests.rs │ └── utils.rs ├── pubspec.yaml ├── scripts ├── bind.android.sh ├── bind.ios.sh ├── bind.linux.sh ├── bind.macos.sh ├── bind.windows.cmd └── thin-payload.sh ├── test └── widget_test.dart └── 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 └── rust.cmake /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.dart] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | native/target 48 | jniLibs 49 | 50 | pubspec.lock 51 | Cargo.lock 52 | bridge_generated.* 53 | version.txt 54 | libnative.a 55 | Podfile.lock 56 | build/ 57 | -------------------------------------------------------------------------------- /.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: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | java liberica-8u322+6 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTML - COMIC 2 | ============ 3 | 4 | A comic client. 5 | 6 | ## Screen Shoots 7 | 8 | ![](images/st01.png) 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "niuhuan.html" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | // 70 | //[ 71 | // new Tuple2('Debug', ''), 72 | // new Tuple2('Profile', '--release'), 73 | // new Tuple2('Release', '--release') 74 | //].each { 75 | // def taskPostfix = it.first 76 | // def profileMode = it.second 77 | // tasks.whenTaskAdded { task -> 78 | // if (task.name == "javaPreCompile$taskPostfix") { 79 | // task.dependsOn "cargoBuild$taskPostfix" 80 | // } 81 | // } 82 | // tasks.register("cargoBuild$taskPostfix", Exec) { 83 | // // Until https://github.com/bbqsrc/cargo-ndk/pull/13 is merged, 84 | // // this workaround is necessary. 85 | // commandLine 'sh', '-c', """cd ../../native && \ 86 | // ANDROID_NDK_HOME="$ANDROID_NDK" cargo ndk \ 87 | // -t armeabi-v7a -t arm64-v8a \ 88 | // -o ../android/app/src/main/jniLibs build $profileMode""" 89 | // } 90 | //} 91 | 92 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 30 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /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.56" 20 | reqwest = { version = "0.11.10", features = ["json"] } 21 | serde = "1.0.136" 22 | serde_derive = "1.0.136" 23 | serde_json = "1.0.79" 24 | tokio = { version = "1.17.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 OWNER: &str = "niuhuan"; 6 | const REPO: &str = "html-comic"; 7 | const UA: &str = "niuhuan html-comic ci"; 8 | const MAIN_BRANCH: &str = "master"; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | // get ghToken 13 | let gh_token = std::env::var("GH_TOKEN")?; 14 | if gh_token.is_empty() { 15 | panic!("Please set GH_TOKEN"); 16 | } 17 | 18 | let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; 19 | let vs_info_txt = tokio::fs::read_to_string("version.info.txt").await?; 20 | 21 | let code = vs_code_txt.trim(); 22 | let info = vs_info_txt.trim(); 23 | 24 | let client = reqwest::ClientBuilder::new().user_agent(UA).build()?; 25 | 26 | let check_response = client 27 | .get(format!( 28 | "https://api.github.com/repos/{}/{}/releases/tags/{}", 29 | OWNER, REPO, code 30 | )) 31 | .send() 32 | .await?; 33 | 34 | match check_response.status().as_u16() { 35 | 200 => { 36 | println!("release exists"); 37 | exit(0); 38 | } 39 | 404 => (), 40 | code => { 41 | let text = check_response.text().await?; 42 | panic!("error for check release : {} : {}", code, text); 43 | } 44 | } 45 | drop(check_response); 46 | 47 | // 404 48 | 49 | let check_response = client 50 | .post(format!( 51 | "https://api.github.com/repos/{}/{}/releases", 52 | OWNER, REPO 53 | )) 54 | .header("Authorization", format!("token {}", gh_token)) 55 | .json(&{ 56 | let mut params = HashMap::::new(); 57 | params.insert("tag_name".to_string(), code.to_string()); 58 | params.insert("target_commitish".to_string(), MAIN_BRANCH.to_string()); 59 | params.insert("name".to_string(), code.to_string()); 60 | params.insert("body".to_string(), info.to_string()); 61 | params 62 | }) 63 | .send() 64 | .await?; 65 | 66 | match check_response.status().as_u16() { 67 | 201 => (), 68 | code => { 69 | let text = check_response.text().await?; 70 | panic!("error for create release : {} : {}", code, text); 71 | } 72 | } 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /ci/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /ci/version.code.txt: -------------------------------------------------------------------------------- 1 | v0.0.1 -------------------------------------------------------------------------------- /ci/version.info.txt: -------------------------------------------------------------------------------- 1 | - Preview version 2 | -------------------------------------------------------------------------------- /images/ic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/images/ic.png -------------------------------------------------------------------------------- /images/st01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/images/st01.png -------------------------------------------------------------------------------- /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 | 9.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, '9.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 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /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 UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | 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(.documentDirectory, .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 | print("dummy_value=\(dummy_method_to_enforce_bundling())"); 52 | GeneratedPluginRegistrant.register(with: self) 53 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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 | NSPhotoLibraryUsageDescription 6 | Usage images 7 | NSPhotoLibraryAddUsageDescription 8 | Save images 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleDisplayName 12 | Flutter Rust Bridge Template 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | html 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | $(FLUTTER_BUILD_NUMBER) 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | #import "bridge_generated.h" 3 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # Homebrew installs LLVM in a place that is not visible to ffigen. 2 | # This explicitly specifies the place where the LLVM dylibs are kept. 3 | llvm_path := if os() == "macos" { 4 | "--llvm-path /opt/homebrew/opt/llvm" 5 | } else { 6 | "" 7 | } 8 | 9 | default: gen lint 10 | 11 | gen: 12 | flutter_rust_bridge_codegen {{llvm_path}} \ 13 | --rust-input native/src/api.rs \ 14 | --dart-output lib/bridge_generated.dart \ 15 | --c-output ios/Runner/bridge_generated.h 16 | cp ios/Runner/bridge_generated.h macos/Runner/bridge_generated.h 17 | # Uncomment this line to invoke build_runner as well 18 | # flutter pub run build_runner build 19 | 20 | lint: 21 | cd native && cargo fmt 22 | dart format . 23 | 24 | clean: 25 | flutter clean 26 | cd native && cargo clean 27 | 28 | # vim:expandtab:sw=4:ts=4 29 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | -------------------------------------------------------------------------------- /lib/assets/android.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 13 | 24 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/lib/assets/error.png -------------------------------------------------------------------------------- /lib/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/lib/assets/init.png -------------------------------------------------------------------------------- /lib/configs/android_display_mode.dart: -------------------------------------------------------------------------------- 1 | /// 显示模式, 仅安卓有效 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:html/cross.dart'; 6 | import 'package:html/ffi.dart'; 7 | 8 | import '../commons.dart'; 9 | 10 | const _propertyName = "androidDisplayMode"; 11 | List _modes = []; 12 | String _androidDisplayMode = ""; 13 | 14 | Future initAndroidDisplayMode() async { 15 | if (Platform.isAndroid) { 16 | _androidDisplayMode = await native.loadProperty(k: _propertyName); 17 | _modes = await cross.loadAndroidModes(); 18 | await _changeMode(); 19 | } 20 | } 21 | 22 | Future _changeMode() async { 23 | await cross.setAndroidMode(_androidDisplayMode); 24 | } 25 | 26 | Future _chooseAndroidDisplayMode(BuildContext context) async { 27 | if (Platform.isAndroid) { 28 | List list = [""]; 29 | list.addAll(_modes); 30 | String? result = await chooseListDialog( 31 | context, 32 | title: "安卓屏幕刷新率", 33 | values: list, 34 | ); 35 | if (result != null) { 36 | await native.saveProperty(k: _propertyName, v: result); 37 | _androidDisplayMode = result; 38 | await _changeMode(); 39 | } 40 | } 41 | } 42 | 43 | Widget androidDisplayModeSetting() { 44 | if (Platform.isAndroid) { 45 | return StatefulBuilder( 46 | builder: (BuildContext context, void Function(void Function()) setState) { 47 | return ListTile( 48 | title: const Text("屏幕刷新率(安卓)"), 49 | subtitle: Text(_androidDisplayMode), 50 | onTap: () async { 51 | await _chooseAndroidDisplayMode(context); 52 | setState(() {}); 53 | }, 54 | ); 55 | }, 56 | ); 57 | } 58 | return Container(); 59 | } 60 | -------------------------------------------------------------------------------- /lib/configs/android_secure_flag.dart: -------------------------------------------------------------------------------- 1 | /// 音量键翻页 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:html/cross.dart'; 6 | import 'package:html/ffi.dart'; 7 | 8 | import '../commons.dart'; 9 | 10 | const _propertyName = "androidSecureFlag"; 11 | 12 | late bool _androidSecureFlag; 13 | 14 | Future initAndroidSecureFlag() async { 15 | if (Platform.isAndroid) { 16 | _androidSecureFlag = 17 | (await native.loadProperty(k: _propertyName)) == "true"; 18 | if (_androidSecureFlag) { 19 | await cross.androidSecureFlag(true); 20 | } 21 | } 22 | } 23 | 24 | Future _chooseAndroidSecureFlag(BuildContext context) async { 25 | String? result = await chooseListDialog( 26 | context, 27 | title: "禁止截图/禁止显示在任务视图", 28 | values: ["是", "否"], 29 | ); 30 | if (result != null) { 31 | var target = result == "是"; 32 | await native.saveProperty(k: _propertyName, v: "$target"); 33 | _androidSecureFlag = target; 34 | await cross.androidSecureFlag(_androidSecureFlag); 35 | } 36 | } 37 | 38 | Widget androidSecureFlagSetting() { 39 | if (Platform.isAndroid) { 40 | return StatefulBuilder(builder: 41 | (BuildContext context, void Function(void Function()) setState) { 42 | return ListTile( 43 | title: const Text("禁止截图/禁止显示在任务视图"), 44 | subtitle: Text(_androidSecureFlag ? "是" : "否"), 45 | onTap: () async { 46 | await _chooseAndroidSecureFlag(context); 47 | setState(() {}); 48 | }); 49 | }); 50 | } 51 | return Container(); 52 | } 53 | -------------------------------------------------------------------------------- /lib/configs/auto_clean.dart: -------------------------------------------------------------------------------- 1 | /// 自动清理 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | const _autoCleanMap = { 6 | "一个月前": "${1000 * 3600 * 24 * 30}", 7 | "一周前": "${1000 * 3600 * 24 * 7}", 8 | "一天前": "${1000 * 3600 * 24 * 1}", 9 | "不自动清理": "${0}", 10 | }; 11 | late String _autoCleanSec; 12 | 13 | Future initAutoClean() async { 14 | _autoCleanSec = await native.loadProperty(k: "autoCleanSec"); 15 | if (_autoCleanSec == "") { 16 | _autoCleanSec = "${3600 * 24 * 30}"; 17 | } 18 | if ("0" != _autoCleanSec) { 19 | await native.autoClean(time: int.parse(_autoCleanSec)); 20 | } 21 | } 22 | 23 | String _currentAutoCleanSec() { 24 | for (var value in _autoCleanMap.entries) { 25 | if (value.value == _autoCleanSec) { 26 | return value.key; 27 | } 28 | } 29 | return ""; 30 | } 31 | 32 | Future _chooseAutoCleanSec(BuildContext context) async { 33 | String? choose = await showDialog( 34 | context: context, 35 | builder: (BuildContext context) { 36 | return SimpleDialog( 37 | title: const Text('选择自动清理周期'), 38 | children: [ 39 | ..._autoCleanMap.entries.map( 40 | (e) => SimpleDialogOption( 41 | child: Text(e.key), 42 | onPressed: () { 43 | Navigator.of(context).pop(e.value); 44 | }, 45 | ), 46 | ), 47 | ], 48 | ); 49 | }, 50 | ); 51 | if (choose != null) { 52 | await native.saveProperty(k: "autoCleanSec", v: choose); 53 | _autoCleanSec = choose; 54 | } 55 | } 56 | 57 | Widget autoCleanSecSetting() { 58 | return StatefulBuilder( 59 | builder: (BuildContext context, void Function(void Function()) setState) { 60 | return ListTile( 61 | title: const Text("自动清理缓存"), 62 | subtitle: Text(_currentAutoCleanSec()), 63 | onTap: () async { 64 | await _chooseAutoCleanSec(context); 65 | setState(() {}); 66 | }, 67 | ); 68 | }, 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /lib/configs/auto_full_screen.dart: -------------------------------------------------------------------------------- 1 | /// 自动全屏 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | const _propertyName = "autoFullScreen"; 8 | late bool _autoFullScreen; 9 | 10 | Future initAutoFullScreen() async { 11 | _autoFullScreen = (await native.loadProperty(k: _propertyName)) == "true"; 12 | } 13 | 14 | bool currentAutoFullScreen() { 15 | return _autoFullScreen; 16 | } 17 | 18 | Future _chooseAutoFullScreen(BuildContext context) async { 19 | String? result = await chooseListDialog(context, 20 | title: "进入阅读器自动全屏", values: ["是", "否"]); 21 | if (result != null) { 22 | var target = result == "是"; 23 | await native.saveProperty(k: _propertyName, v: "$target"); 24 | _autoFullScreen = target; 25 | } 26 | } 27 | 28 | Widget autoFullScreenSetting() { 29 | return StatefulBuilder( 30 | builder: (BuildContext context, void Function(void Function()) setState) { 31 | return ListTile( 32 | title: const Text("进入阅读器自动全屏"), 33 | subtitle: Text(_autoFullScreen ? "是" : "否"), 34 | onTap: () async { 35 | await _chooseAutoFullScreen(context); 36 | setState(() {}); 37 | }, 38 | ); 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /lib/configs/content_failed_reload_action.dart: -------------------------------------------------------------------------------- 1 | /// 全屏操作 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | enum ContentFailedReloadAction { 8 | pullDown, 9 | touchLoader, 10 | } 11 | 12 | const _propertyName = "contentFailedReloadAction"; 13 | late ContentFailedReloadAction contentFailedReloadAction; 14 | 15 | Future initContentFailedReloadAction() async { 16 | var value = await native.loadProperty(k: _propertyName); 17 | if (value == "") { 18 | value = ContentFailedReloadAction.pullDown.toString(); 19 | } 20 | contentFailedReloadAction = _contentFailedReloadActionFromString(value); 21 | } 22 | 23 | ContentFailedReloadAction _contentFailedReloadActionFromString(String string) { 24 | for (var value in ContentFailedReloadAction.values) { 25 | if (string == value.toString()) { 26 | return value; 27 | } 28 | } 29 | return ContentFailedReloadAction.pullDown; 30 | } 31 | 32 | Map _contentFailedReloadActionMap = { 33 | "下拉刷新": ContentFailedReloadAction.pullDown, 34 | "点击屏幕刷新": ContentFailedReloadAction.touchLoader, 35 | }; 36 | 37 | String _currentContentFailedReloadActionName() { 38 | for (var e in _contentFailedReloadActionMap.entries) { 39 | if (e.value == contentFailedReloadAction) { 40 | return e.key; 41 | } 42 | } 43 | return ''; 44 | } 45 | 46 | Future _chooseContentFailedReloadAction(BuildContext context) async { 47 | ContentFailedReloadAction? result = 48 | await chooseMapDialog( 49 | context, 50 | values: _contentFailedReloadActionMap, 51 | title: "选择页面加载失败刷新的方式", 52 | ); 53 | if (result != null) { 54 | await native.saveProperty(k: _propertyName, v: result.toString()); 55 | contentFailedReloadAction = result; 56 | } 57 | } 58 | 59 | Widget contentFailedReloadActionSetting() { 60 | return StatefulBuilder( 61 | builder: (BuildContext context, void Function(void Function()) setState) { 62 | return ListTile( 63 | title: const Text("加载失败时"), 64 | subtitle: Text(_currentContentFailedReloadActionName()), 65 | onTap: () async { 66 | await _chooseContentFailedReloadAction(context); 67 | setState(() {}); 68 | }, 69 | ); 70 | }, 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /lib/configs/export_rename.dart: -------------------------------------------------------------------------------- 1 | /// 自动全屏 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | const _propertyName = "exportRename"; 8 | late bool _exportRename; 9 | 10 | Future initExportRename() async { 11 | _exportRename = (await native.loadProperty(k: _propertyName)) == "true"; 12 | } 13 | 14 | bool currentExportRename() { 15 | return _exportRename; 16 | } 17 | 18 | Future _chooseExportRename(BuildContext context) async { 19 | String? result = await chooseListDialog( 20 | context, 21 | title: "导出时进行重命名", 22 | values: ["是", "否"], 23 | ); 24 | if (result != null) { 25 | var target = result == "是"; 26 | await native.saveProperty(k: _propertyName, v: "$target"); 27 | _exportRename = target; 28 | } 29 | } 30 | 31 | Widget exportRenameSetting() { 32 | return StatefulBuilder( 33 | builder: (BuildContext context, void Function(void Function()) setState) { 34 | return ListTile( 35 | title: const Text("导出时进行重命名"), 36 | subtitle: Text(_exportRename ? "是" : "否"), 37 | onTap: () async { 38 | await _chooseExportRename(context); 39 | setState(() {}); 40 | }, 41 | ); 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/configs/full_screen_action.dart: -------------------------------------------------------------------------------- 1 | /// 全屏操作 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | import '../commons.dart'; 5 | 6 | enum FullScreenAction { 7 | touchOnce, 8 | controller, 9 | touchDouble, 10 | touchDoubleOnceNext, 11 | threeArea, 12 | } 13 | 14 | Map _fullScreenActionMap = { 15 | "点击屏幕一次全屏": FullScreenAction.touchOnce, 16 | "使用控制器全屏": FullScreenAction.controller, 17 | "双击屏幕全屏": FullScreenAction.touchDouble, 18 | "双击屏幕全屏 + 单击屏幕下一页": FullScreenAction.touchDoubleOnceNext, 19 | "将屏幕划分成三个区域 (上一页, 下一页, 全屏)": FullScreenAction.threeArea, 20 | }; 21 | 22 | const _defaultController = FullScreenAction.touchOnce; 23 | const _propertyName = "fullScreenAction"; 24 | late FullScreenAction _fullScreenAction; 25 | 26 | Future initFullScreenAction() async { 27 | var value = await native.loadProperty(k: _propertyName); 28 | if (value == "") value = FullScreenAction.touchOnce.toString(); 29 | _fullScreenAction = _fullScreenActionFromString(value); 30 | } 31 | 32 | FullScreenAction get currentFullScreenAction => _fullScreenAction; 33 | 34 | FullScreenAction _fullScreenActionFromString(String string) { 35 | for (var value in FullScreenAction.values) { 36 | if (string == value.toString()) { 37 | return value; 38 | } 39 | } 40 | return _defaultController; 41 | } 42 | 43 | String currentFullScreenActionName() { 44 | for (var e in _fullScreenActionMap.entries) { 45 | if (e.value == _fullScreenAction) { 46 | return e.key; 47 | } 48 | } 49 | return ''; 50 | } 51 | 52 | Future chooseFullScreenAction(BuildContext context) async { 53 | FullScreenAction? result = await chooseMapDialog(context, 54 | values: _fullScreenActionMap, title: "选择操控方式"); 55 | if (result != null) { 56 | await native.saveProperty(k: _propertyName, v: result.toString()); 57 | _fullScreenAction = result; 58 | } 59 | } 60 | 61 | Widget fullScreenActionSetting() { 62 | return StatefulBuilder( 63 | builder: (BuildContext context, void Function(void Function()) setState) { 64 | return ListTile( 65 | title: const Text("操控方式"), 66 | subtitle: Text(currentFullScreenActionName()), 67 | onTap: () async { 68 | await chooseFullScreenAction(context); 69 | setState(() {}); 70 | }, 71 | ); 72 | }, 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /lib/configs/gallery_preload_count.dart: -------------------------------------------------------------------------------- 1 | /// 相册模式下预加载图片数量 2 | 3 | const galleryPrePreloadCount = 1; 4 | const galleryPreloadCount = 2; 5 | -------------------------------------------------------------------------------- /lib/configs/keyboard_controller.dart: -------------------------------------------------------------------------------- 1 | /// 上下键翻页 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:html/ffi.dart'; 6 | 7 | import '../commons.dart'; 8 | 9 | const _propertyName = "keyboardController"; 10 | 11 | late bool keyboardController; 12 | 13 | Future initKeyboardController() async { 14 | keyboardController = (await native.loadProperty(k: _propertyName)) == "true"; 15 | } 16 | 17 | Future _chooseKeyboardController(BuildContext context) async { 18 | String? result = await chooseListDialog( 19 | context, 20 | title: "键盘控制翻页", 21 | values: ["是", "否"], 22 | ); 23 | if (result != null) { 24 | var target = result == "是"; 25 | await native.saveProperty(k: _propertyName, v: "$target"); 26 | keyboardController = target; 27 | } 28 | } 29 | 30 | Widget keyboardControllerSetting() { 31 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 32 | return StatefulBuilder( 33 | builder: (BuildContext context, void Function(void Function()) setState) { 34 | return ListTile( 35 | title: const Text("阅读器键盘翻页(仅PC)"), 36 | subtitle: Text(keyboardController ? "是" : "否"), 37 | onTap: () async { 38 | await _chooseKeyboardController(context); 39 | setState(() {}); 40 | }, 41 | ); 42 | }, 43 | ); 44 | } 45 | return Container(); 46 | } 47 | -------------------------------------------------------------------------------- /lib/configs/list_layout.dart: -------------------------------------------------------------------------------- 1 | /// 列表页的布局 2 | import 'package:event/event.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:html/ffi.dart'; 5 | 6 | import '../commons.dart'; 7 | 8 | enum ListLayout { 9 | infoCard, 10 | onlyImage, 11 | coverAndTitle, 12 | } 13 | 14 | const Map _listLayoutMap = { 15 | '详情': ListLayout.infoCard, 16 | '封面': ListLayout.onlyImage, 17 | '封面+标题': ListLayout.coverAndTitle, 18 | }; 19 | 20 | const _propertyName = "listLayout"; 21 | late ListLayout currentLayout; 22 | 23 | var listLayoutEvent = Event(); 24 | 25 | Future initListLayout() async { 26 | var value = await native.loadProperty( 27 | k: _propertyName, 28 | ); 29 | if (value == "") value = ListLayout.infoCard.toString(); 30 | currentLayout = _listLayoutFromString(value); 31 | } 32 | 33 | ListLayout _listLayoutFromString(String layoutString) { 34 | for (var value in ListLayout.values) { 35 | if (layoutString == value.toString()) { 36 | return value; 37 | } 38 | } 39 | return ListLayout.infoCard; 40 | } 41 | 42 | void _chooseListLayout(BuildContext context) async { 43 | ListLayout? layout = await chooseMapDialog( 44 | context, 45 | values: _listLayoutMap, 46 | title: '请选择布局', 47 | ); 48 | if (layout != null) { 49 | await native.saveProperty(k: _propertyName, v: layout.toString()); 50 | currentLayout = layout; 51 | listLayoutEvent.broadcast(); 52 | } 53 | } 54 | 55 | IconButton chooseLayoutActionButton(BuildContext context) => IconButton( 56 | onPressed: () { 57 | _chooseListLayout(context); 58 | }, 59 | icon: const Icon(Icons.view_quilt), 60 | ); 61 | -------------------------------------------------------------------------------- /lib/configs/no_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/ffi.dart'; 3 | 4 | import '../commons.dart'; 5 | 6 | const _propertyName = "noAnimation"; 7 | 8 | late bool _noAnimation; 9 | 10 | Future initNoAnimation() async { 11 | _noAnimation = (await native.loadProperty(k: _propertyName)) == "true"; 12 | } 13 | 14 | bool noAnimation() { 15 | return _noAnimation; 16 | } 17 | 18 | Future _chooseNoAnimation(BuildContext context) async { 19 | String? result = await chooseListDialog( 20 | context, 21 | title: "取消键盘或音量翻页动画", 22 | values: ["是", "否"], 23 | ); 24 | if (result != null) { 25 | var target = result == "是"; 26 | await native.saveProperty(k: _propertyName, v: "$target"); 27 | _noAnimation = target; 28 | } 29 | } 30 | 31 | Widget noAnimationSetting() { 32 | return StatefulBuilder( 33 | builder: (BuildContext context, void Function(void Function()) setState) { 34 | return ListTile( 35 | title: const Text("取消键盘或音量翻页动画"), 36 | subtitle: Text(_noAnimation ? "是" : "否"), 37 | onTap: () async { 38 | await _chooseNoAnimation(context); 39 | setState(() {}); 40 | }, 41 | ); 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/configs/pager_action.dart: -------------------------------------------------------------------------------- 1 | /// 列表页下一页的行为 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../commons.dart'; 5 | import '../ffi.dart'; 6 | 7 | enum PagerAction { 8 | controller, 9 | stream, 10 | } 11 | 12 | Map _pagerActionMap = { 13 | "使用按钮": PagerAction.controller, 14 | "瀑布流": PagerAction.stream, 15 | }; 16 | 17 | const _propertyName = "pagerAction"; 18 | late PagerAction _pagerAction; 19 | 20 | Future initPagerAction() async { 21 | var value = await native.loadProperty(k: _propertyName); 22 | if (value == "") value = PagerAction.controller.toString(); 23 | _pagerAction = _pagerActionFromString(value); 24 | } 25 | 26 | PagerAction currentPagerAction() { 27 | return _pagerAction; 28 | } 29 | 30 | PagerAction _pagerActionFromString(String string) { 31 | for (var value in PagerAction.values) { 32 | if (string == value.toString()) { 33 | return value; 34 | } 35 | } 36 | return PagerAction.controller; 37 | } 38 | 39 | String _currentPagerActionName() { 40 | for (var e in _pagerActionMap.entries) { 41 | if (e.value == _pagerAction) { 42 | return e.key; 43 | } 44 | } 45 | return ''; 46 | } 47 | 48 | Future _choosePagerAction(BuildContext context) async { 49 | PagerAction? result = await chooseMapDialog( 50 | context, 51 | values: _pagerActionMap, 52 | title: "选择列表页加载方式", 53 | ); 54 | if (result != null) { 55 | await native.saveProperty(k: _propertyName, v: result.toString()); 56 | _pagerAction = result; 57 | } 58 | } 59 | 60 | Widget pagerActionSetting() { 61 | return StatefulBuilder( 62 | builder: (BuildContext context, void Function(void Function()) setState) { 63 | return ListTile( 64 | title: const Text("列表页加载方式"), 65 | subtitle: Text(_currentPagerActionName()), 66 | onTap: () async { 67 | await _choosePagerAction(context); 68 | setState(() {}); 69 | }, 70 | ); 71 | }, 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/configs/platform.dart: -------------------------------------------------------------------------------- 1 | /// 平台信息 2 | import 'dart:io'; 3 | 4 | import '../cross.dart'; 5 | 6 | int androidVersion = 0; 7 | 8 | Future initPlatform() async { 9 | if (Platform.isAndroid) { 10 | androidVersion = await cross.androidGetVersion(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/configs/proxy.dart: -------------------------------------------------------------------------------- 1 | /// 代理设置 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import '../commons.dart'; 6 | 7 | late String _currentProxy; 8 | const String _k = "proxy"; 9 | 10 | Future initProxy() async { 11 | _currentProxy = await native.loadProperty(k: _k); 12 | await native.setProxy(url: _currentProxy); 13 | } 14 | 15 | String currentProxyName() { 16 | return _currentProxy == "" ? "未设置" : _currentProxy; 17 | } 18 | 19 | Future inputProxy(BuildContext context) async { 20 | String? input = await displayTextInputDialog( 21 | context, 22 | src: _currentProxy, 23 | title: AppLocalizations.of(context)!.proxy, 24 | hint: AppLocalizations.of(context)!.inputProxy, 25 | desc: AppLocalizations.of(context)!.proxyExample, 26 | ); 27 | if (input != null) { 28 | await native.setProxy(url: input); 29 | await native.saveProperty(k: _k, v: 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: Text(AppLocalizations.of(context)!.proxy), 39 | subtitle: Text(currentProxyName()), 40 | onTap: () async { 41 | await inputProxy(context); 42 | setState(() {}); 43 | }, 44 | ); 45 | }, 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /lib/configs/reader_direction.dart: -------------------------------------------------------------------------------- 1 | /// 阅读器的方向 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | enum ReaderDirection { 6 | topToBottom, 7 | leftToRight, 8 | rightToLeft, 9 | } 10 | 11 | const _types = { 12 | '从上到下': ReaderDirection.topToBottom, 13 | '从左到右': ReaderDirection.leftToRight, 14 | '从右到左': ReaderDirection.rightToLeft, 15 | }; 16 | 17 | const _propertyName = "readerDirection"; 18 | late ReaderDirection _readerDirection; 19 | 20 | ReaderDirection get gReaderDirection => _readerDirection; 21 | 22 | Future initReaderDirection() async { 23 | var value = await native.loadProperty(k: _propertyName); 24 | if (value == "") { 25 | value = ReaderDirection.topToBottom.toString(); 26 | } 27 | _readerDirection = _pagerDirectionFromString(value); 28 | } 29 | 30 | ReaderDirection _pagerDirectionFromString(String pagerDirectionString) { 31 | for (var value in ReaderDirection.values) { 32 | if (pagerDirectionString == value.toString()) { 33 | return value; 34 | } 35 | } 36 | return ReaderDirection.topToBottom; 37 | } 38 | 39 | String currentReaderDirectionName() { 40 | for (var e in _types.entries) { 41 | if (e.value == _readerDirection) { 42 | return e.key; 43 | } 44 | } 45 | return ''; 46 | } 47 | 48 | /// ?? to ActionButton And Event ?? 49 | Future choosePagerDirection(BuildContext buildContext) async { 50 | ReaderDirection? choose = await showDialog( 51 | context: buildContext, 52 | builder: (BuildContext context) { 53 | return SimpleDialog( 54 | title: const Text("选择翻页方向"), 55 | children: _types.entries 56 | .map((e) => SimpleDialogOption( 57 | child: Text(e.key), 58 | onPressed: () { 59 | Navigator.of(context).pop(e.value); 60 | }, 61 | )) 62 | .toList(), 63 | ); 64 | }, 65 | ); 66 | if (choose != null) { 67 | await native.saveProperty(k: _propertyName, v: choose.toString()); 68 | _readerDirection = choose; 69 | } 70 | } 71 | 72 | Widget readerDirectionSetting() { 73 | return StatefulBuilder( 74 | builder: (BuildContext context, void Function(void Function()) setState) { 75 | return ListTile( 76 | title: const Text("阅读器方向"), 77 | subtitle: Text(currentReaderDirectionName()), 78 | onTap: () async { 79 | await choosePagerDirection(context); 80 | setState(() {}); 81 | }, 82 | ); 83 | }, 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /lib/configs/reader_slider_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/ffi.dart'; 3 | 4 | import '../commons.dart'; 5 | 6 | enum ReaderSliderPosition { bottom, right, left } 7 | 8 | const _positionNames = { 9 | ReaderSliderPosition.bottom: '下方', 10 | ReaderSliderPosition.right: '右侧', 11 | ReaderSliderPosition.left: '左侧', 12 | }; 13 | 14 | const _propertyName = "readerSliderPosition"; 15 | late ReaderSliderPosition _readerSliderPosition; 16 | 17 | Future initReaderSliderPosition() async { 18 | _readerSliderPosition = _readerSliderPositionFromString( 19 | await native.loadProperty(k: _propertyName), 20 | ); 21 | } 22 | 23 | ReaderSliderPosition _readerSliderPositionFromString(String str) { 24 | for (var value in ReaderSliderPosition.values) { 25 | if (str == value.toString()) return value; 26 | } 27 | return ReaderSliderPosition.bottom; 28 | } 29 | 30 | ReaderSliderPosition get currentReaderSliderPosition => _readerSliderPosition; 31 | 32 | String currentReaderSliderPositionName() => 33 | _positionNames[_readerSliderPosition] ?? ""; 34 | 35 | Future chooseReaderSliderPosition(BuildContext context) async { 36 | Map map = {}; 37 | _positionNames.forEach((key, value) { 38 | map[value] = key; 39 | }); 40 | ReaderSliderPosition? result = await chooseMapDialog( 41 | context, 42 | values: map, 43 | title: "选择滑动条位置"); 44 | if (result != null) { 45 | await native.saveProperty(k: _propertyName, v: result.toString()); 46 | _readerSliderPosition = result; 47 | } 48 | } 49 | 50 | Widget readerSliderPositionSetting() { 51 | return StatefulBuilder( 52 | builder: (BuildContext context, void Function(void Function()) setState) { 53 | return ListTile( 54 | title: const Text("滚动条的位置"), 55 | subtitle: Text(currentReaderSliderPositionName()), 56 | onTap: () async { 57 | await chooseReaderSliderPosition(context); 58 | setState(() {}); 59 | }, 60 | ); 61 | }, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /lib/configs/reader_type.dart: -------------------------------------------------------------------------------- 1 | /// 阅读器的类型 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | enum ReaderType { 6 | webToon, 7 | webToonZoom, 8 | gallery, 9 | webToonFreeZoom, 10 | } 11 | 12 | const _types = { 13 | 'WebToon (默认)': ReaderType.webToon, 14 | 'WebToon (双击放大)': ReaderType.webToonZoom, 15 | '相册': ReaderType.gallery, 16 | 'WebToon (ListView双击放大)\n(此模式进度条无效)': ReaderType.webToonFreeZoom 17 | }; 18 | 19 | const _propertyName = "readerType"; 20 | late ReaderType _readerType; 21 | 22 | Future initReaderType() async { 23 | var value = await native.loadProperty(k: _propertyName); 24 | if (value == "") value = ReaderType.webToon.toString(); 25 | _readerType = _readerTypeFromString(value); 26 | } 27 | 28 | ReaderType get currentReaderType => _readerType; 29 | 30 | ReaderType _readerTypeFromString(String pagerTypeString) { 31 | for (var value in ReaderType.values) { 32 | if (pagerTypeString == value.toString()) { 33 | return value; 34 | } 35 | } 36 | return ReaderType.webToon; 37 | } 38 | 39 | String currentReaderTypeName() { 40 | for (var e in _types.entries) { 41 | if (e.value == _readerType) { 42 | return e.key; 43 | } 44 | } 45 | return ''; 46 | } 47 | 48 | Future choosePagerType(BuildContext buildContext) async { 49 | ReaderType? t = await showDialog( 50 | context: buildContext, 51 | builder: (BuildContext context) { 52 | return SimpleDialog( 53 | title: const Text("选择阅读模式"), 54 | children: _types.entries 55 | .map((e) => SimpleDialogOption( 56 | child: Text(e.key), 57 | onPressed: () { 58 | Navigator.of(context).pop(e.value); 59 | }, 60 | )) 61 | .toList(), 62 | ); 63 | }, 64 | ); 65 | if (t != null) { 66 | await native.saveProperty(k: _propertyName, v: t.toString()); 67 | _readerType = t; 68 | } 69 | } 70 | 71 | Widget readerTypeSetting() { 72 | return StatefulBuilder( 73 | builder: (BuildContext context, void Function(void Function()) setState) { 74 | return ListTile( 75 | title: const Text("阅读器模式"), 76 | subtitle: Text(currentReaderTypeName()), 77 | onTap: () async { 78 | await choosePagerType(context); 79 | setState(() {}); 80 | }, 81 | ); 82 | }, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /lib/configs/time_offset_hour.dart: -------------------------------------------------------------------------------- 1 | /// 时区设置 2 | import 'package:flutter/material.dart'; 3 | import 'package:html/ffi.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | const _propertyName = "timeOffsetHour"; 8 | int _timeOffsetHour = 8; 9 | 10 | Future initTimeZone() async { 11 | var value = await native.loadProperty(k: _propertyName); 12 | if (value == "") value = "8"; 13 | _timeOffsetHour = int.parse(value); 14 | } 15 | 16 | int currentTimeOffsetHour() { 17 | return _timeOffsetHour; 18 | } 19 | 20 | Future _chooseTimeZone(BuildContext context) async { 21 | List timeZones = []; 22 | for (var i = -12; i <= 12; i++) { 23 | var str = i.toString(); 24 | if (!str.startsWith("-")) { 25 | str = "+" + str; 26 | } 27 | timeZones.add(str); 28 | } 29 | String? result = await chooseListDialog( 30 | context, 31 | title: "时区选择", 32 | values: timeZones, 33 | ); 34 | if (result != null) { 35 | if (result.startsWith("+")) { 36 | result = result.substring(1); 37 | } 38 | _timeOffsetHour = int.parse(result); 39 | await native.saveProperty(k: _propertyName, v: result); 40 | } 41 | } 42 | 43 | Widget timeZoneSetting() { 44 | return StatefulBuilder( 45 | builder: (BuildContext context, void Function(void Function()) setState) { 46 | var c = "$_timeOffsetHour"; 47 | if (!c.startsWith("-")) { 48 | c = "+" + c; 49 | } 50 | return ListTile( 51 | title: const Text("时区"), 52 | subtitle: Text(c), 53 | onTap: () async { 54 | await _chooseTimeZone(context); 55 | setState(() {}); 56 | }, 57 | ); 58 | }, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /lib/configs/version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:convert'; 3 | 4 | import 'package:event/event.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart' show rootBundle; 7 | import 'package:html/ffi.dart'; 8 | 9 | import '../commons.dart'; 10 | 11 | const _versionUrl = 12 | "https://api.github.com/repos/niuhuan/html-comic/releases/latest"; 13 | const _versionAssets = 'lib/assets/version.txt'; 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? latestVersion() { 35 | if (_latestVersion == _version) { 36 | return null; 37 | } 38 | return _latestVersion; 39 | } 40 | 41 | String? latestVersionInfo() { 42 | if (_latestVersion == _version) { 43 | return null; 44 | } 45 | return _latestVersionInfo; 46 | } 47 | 48 | Future autoCheckNewVersion() { 49 | return _versionCheck(); 50 | } 51 | 52 | Future manualCheckNewVersion(BuildContext context) async { 53 | try { 54 | defaultToast(context, "检查更新中"); 55 | await _versionCheck(); 56 | defaultToast(context, "检查更新成功"); 57 | } catch (e) { 58 | defaultToast(context, "检查更新失败 : $e"); 59 | } 60 | } 61 | 62 | bool dirtyVersion() { 63 | return "dirty" == _version; 64 | } 65 | 66 | // maybe exception 67 | Future _versionCheck() async { 68 | if (!dirtyVersion()) { 69 | // 检查更新只能使用defaultHttpClient, 而不能使用pika的client, 否则会 "tls handshake failure" 70 | var json = jsonDecode(await native.httpGet(url: _versionUrl)); 71 | if (json["name"] != null) { 72 | String latestVersion = (json["name"]); 73 | if (latestVersion != _version) { 74 | _latestVersion = latestVersion; 75 | _latestVersionInfo = json["body"] ?? ""; 76 | } 77 | } 78 | } // else dirtyVersion 79 | versionEvent.broadcast(); 80 | } 81 | -------------------------------------------------------------------------------- /lib/configs/volume_controller.dart: -------------------------------------------------------------------------------- 1 | /// 音量键翻页 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:html/ffi.dart'; 6 | 7 | import '../commons.dart'; 8 | 9 | const _propertyName = "volumeController"; 10 | late bool volumeController; 11 | 12 | Future initVolumeController() async { 13 | volumeController = (await native.loadProperty(k: _propertyName)) == "true"; 14 | } 15 | 16 | Future _chooseVolumeController(BuildContext context) async { 17 | String? result = await chooseListDialog( 18 | context, 19 | title: "音量键控制翻页", 20 | values: ["是", "否"], 21 | ); 22 | if (result != null) { 23 | var target = result == "是"; 24 | await native.saveProperty(k: _propertyName, v: "$target"); 25 | volumeController = target; 26 | } 27 | } 28 | 29 | Widget volumeControllerSetting() { 30 | if (Platform.isAndroid) { 31 | return StatefulBuilder(builder: 32 | (BuildContext context, void Function(void Function()) setState) { 33 | return ListTile( 34 | title: const Text("阅读器音量键翻页"), 35 | subtitle: Text(volumeController ? "是" : "否"), 36 | onTap: () async { 37 | await _chooseVolumeController(context); 38 | setState(() {}); 39 | }); 40 | }); 41 | } 42 | return Container(); 43 | } 44 | -------------------------------------------------------------------------------- /lib/cross.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 7 | import 'package:permission_handler/permission_handler.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | // todo save origin image in android // like with filesystem_picker 11 | 12 | import 'commons.dart'; 13 | import 'ffi.dart'; 14 | 15 | const cross = Cross._(); 16 | 17 | class Cross { 18 | const Cross._(); 19 | 20 | static const _channel = MethodChannel("cross"); 21 | 22 | Future root(BuildContext context) async { 23 | if (Platform.isAndroid || Platform.isIOS) { 24 | return await _channel.invokeMethod("root"); 25 | } 26 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 27 | return await native.desktopRoot(); 28 | } 29 | throw AppLocalizations.of(context)!.unsupportedPlatform; 30 | } 31 | 32 | Future saveImageFileToGallery(String path, BuildContext context) async { 33 | if (Platform.isIOS || Platform.isAndroid) { 34 | if (Platform.isAndroid) { 35 | if (!(await Permission.storage.request()).isGranted) { 36 | return; 37 | } 38 | } 39 | try { 40 | await _channel.invokeMethod("saveImageToGallery", path); 41 | defaultToast(context, AppLocalizations.of(context)!.success); 42 | } catch (e) { 43 | errorToast(context, AppLocalizations.of(context)!.failed + " : $e"); 44 | } 45 | } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 46 | String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); 47 | if (selectedDirectory != null) { 48 | try { 49 | await native.copyImageTo(srcPath: path, toDir: selectedDirectory); 50 | defaultToast(context, AppLocalizations.of(context)!.success); 51 | } catch (e) { 52 | errorToast(context, AppLocalizations.of(context)!.failed + " : $e"); 53 | } 54 | } 55 | } 56 | } 57 | 58 | Future> loadAndroidModes() async { 59 | return List.of(await _channel.invokeMethod("androidGetModes")) 60 | .map((e) => "$e") 61 | .toList(); 62 | } 63 | 64 | Future setAndroidMode(String androidDisplayMode) { 65 | return _channel 66 | .invokeMethod("androidSetMode", {"mode": androidDisplayMode}); 67 | } 68 | 69 | Future androidSecureFlag(bool flag) { 70 | return _channel.invokeMethod("androidSecureFlag", { 71 | "flag": flag, 72 | }); 73 | } 74 | 75 | Future androidGetVersion() async { 76 | return await _channel.invokeMethod("androidGetVersion", {}); 77 | } 78 | } 79 | 80 | /// 打开web页面 81 | Future openUrl(String url) async { 82 | if (await canLaunch(url)) { 83 | await launch( 84 | url, 85 | forceSafariVC: false, 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/ffi.dart: -------------------------------------------------------------------------------- 1 | // This file initializes the dynamic library and connects it with the stub 2 | // generated by flutter_rust_bridge_codegen. 3 | 4 | import 'dart:ffi'; 5 | 6 | import 'bridge_generated.dart'; 7 | 8 | // Re-export the bridge so it is only necessary to import this file. 9 | export 'bridge_generated.dart'; 10 | import 'dart:io' as io; 11 | 12 | const _base = 'native'; 13 | 14 | // On MacOS, the dynamic library is not bundled with the binary, 15 | // but rather directly **linked** against the binary. 16 | final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so'; 17 | 18 | // The late modifier delays initializing the value until it is actually needed, 19 | // leaving precious little time for the program to quickly start up. 20 | late final Native native = NativeImpl(io.Platform.isIOS || io.Platform.isMacOS 21 | ? DynamicLibrary.executable() 22 | : DynamicLibrary.open(_dylib)); 23 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "unsupportedPlatform":"Unsupported platform", 3 | "ok":"Ok", 4 | "cancel":"Cancel", 5 | "success":"Success", 6 | "failed":"Failed", 7 | "copy":"Copy", 8 | "copied":"Copied", 9 | "choose":"Choose", 10 | "previewImage":"Preview image", 11 | "saveImage": "Save image", 12 | "tags":"Tags", 13 | "nextPage": "Next", 14 | "prePage": "PRE", 15 | "inputPageNumber": "InputPageNumber", 16 | "pages": "Pages", 17 | "loading": "Loading", 18 | "errorTapToRefresh": "Error, tap to refresh", 19 | "refresh": "Refresh", 20 | "connectExceptionCheckNetwork": "Connect exception, check network", 21 | "illegalPermissions": "Illegal permissions", 22 | "checkDeviceTime": "Check device time", 23 | "hasBroken": "Oh, has broken", 24 | "tapToRefresh": "Tap to refresh", 25 | "pullDownToRefresh": "Pull down to refresh", 26 | "settings": "Settings", 27 | "theme": "Theme", 28 | "themeDark": "Theme (dark)", 29 | "enableDarkMode": "Another theme in dark mode", 30 | "clearCache": "Clear cache", 31 | "about": "About", 32 | "proxy": "Proxy", 33 | "inputProxy": "Input proxy", 34 | "proxyExample": " ( Like socks5://127.0.0.1:1080/ ) " 35 | } 36 | -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "unsupportedPlatform":"不支持的平台", 3 | "ok":"确定", 4 | "cancel":"确定", 5 | "success":"成功", 6 | "failed":"失败", 7 | "copy":"复制", 8 | "copied":"已复制", 9 | "choose":"请选择", 10 | "previewImage":"预览图片", 11 | "saveImage": "保存图片", 12 | "tags":"标签", 13 | "nextPage": "下一页", 14 | "prePage": "上一页", 15 | "inputPageNumber": "请输入页数", 16 | "pages": "分页", 17 | "loading": "加载中", 18 | "errorTapToRefresh": "出错啦, 点击刷新", 19 | "refresh": "刷新", 20 | "connectExceptionCheckNetwork": "连接不上啦, 请检查网络", 21 | "illegalPermissions": "没有权限或路径不可用", 22 | "checkDeviceTime": "请检查设备时间", 23 | "hasBroken": "啊哦, 被玩坏了", 24 | "tapToRefresh": "点击刷新", 25 | "pullDownToRefresh": "下拉刷新", 26 | "settings": "设置", 27 | "theme": "主题", 28 | "themeDark": "主题(深色)", 29 | "enableDarkMode": "深色模式下使用不同的主题", 30 | "clearCache": "清除缓存", 31 | "about": "关于", 32 | "proxy": "代理", 33 | "inputProxy": "请输入代理", 34 | "proxyExample": " ( 例如 socks5://127.0.0.1:1080/ ) " 35 | } 36 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:html/configs/themes.dart'; 5 | import 'package:html/screens/components/mouse_and_touch_scroll_behavior.dart'; 6 | import 'package:html/screens/components/navigator.dart'; 7 | import 'package:html/screens/init_screen.dart'; 8 | 9 | void main() { 10 | runApp(const MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | const MyApp({Key? key}) : super(key: key); 15 | 16 | // This widget is the root of your application. 17 | @override 18 | Widget build(BuildContext context) { 19 | return MaterialApp( 20 | theme: currentLightThemeData(), 21 | darkTheme: currentDarkThemeData(), 22 | navigatorObservers: [ 23 | routeObserver, 24 | navigatorObserver, 25 | ], 26 | debugShowCheckedModeBanner: false, 27 | scrollBehavior: mouseAndTouchScrollBehavior, 28 | localizationsDelegates: const [ 29 | AppLocalizations.delegate, 30 | GlobalMaterialLocalizations.delegate, 31 | GlobalWidgetsLocalizations.delegate, 32 | GlobalCupertinoLocalizations.delegate, 33 | ], 34 | supportedLocales: AppLocalizations.supportedLocales, 35 | home: const InitScreen(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/screens/comics_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/ffi.dart'; 3 | import 'package:html/screens/components/badge.dart'; 4 | import 'package:html/screens/components/comic_pager.dart'; 5 | import 'package:html/screens/settings_screen.dart'; 6 | 7 | class ComicsScreen extends StatefulWidget { 8 | const ComicsScreen({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _ComicsScreenState(); 12 | } 13 | 14 | class _ComicsScreenState extends State { 15 | Future _fetchPage(int offset, int limit) async { 16 | return native.comics( 17 | sortType: 'index', 18 | lang: 'all', 19 | offset: offset, 20 | limit: limit, 21 | ); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | actions: [ 29 | IconButton( 30 | onPressed: () { 31 | Navigator.push( 32 | context, 33 | MaterialPageRoute(builder: (BuildContext context) { 34 | return const SettingsScreen(); 35 | }), 36 | ); 37 | }, 38 | icon: const VersionBadged( 39 | child: Icon(Icons.settings), 40 | ), 41 | ), 42 | ], 43 | ), 44 | body: ComicPager(fetchPage: _fetchPage), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/screens/components/badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../configs/version.dart'; 4 | 5 | // 提示信息, 组件右上角的小红点 6 | class Badged extends StatelessWidget { 7 | final String? badge; 8 | final Widget child; 9 | 10 | const Badged({Key? key, required this.child, this.badge}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | if (badge == null) { 15 | return child; 16 | } 17 | return Stack( 18 | children: [ 19 | child, 20 | Positioned( 21 | right: 0, 22 | child: Container( 23 | padding: const EdgeInsets.all(1), 24 | decoration: BoxDecoration( 25 | color: Colors.red, 26 | borderRadius: BorderRadius.circular(6), 27 | ), 28 | constraints: const BoxConstraints( 29 | minWidth: 12, 30 | minHeight: 12, 31 | ), 32 | child: Text( 33 | badge!, 34 | style: const TextStyle( 35 | color: Colors.white, 36 | fontSize: 8, 37 | ), 38 | textAlign: TextAlign.center, 39 | ), 40 | ), 41 | ), 42 | ], 43 | ); 44 | } 45 | } 46 | 47 | class VersionBadged extends StatefulWidget { 48 | final Widget child; 49 | 50 | const VersionBadged({required this.child, Key? key}) : super(key: key); 51 | 52 | @override 53 | State createState() => _VersionBadgedState(); 54 | } 55 | 56 | class _VersionBadgedState extends State { 57 | @override 58 | void initState() { 59 | versionEvent.subscribe(_onVersion); 60 | super.initState(); 61 | } 62 | 63 | @override 64 | void dispose() { 65 | versionEvent.unsubscribe(_onVersion); 66 | super.dispose(); 67 | } 68 | 69 | void _onVersion(dynamic a) { 70 | setState(() {}); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return Badged( 76 | child: widget.child, 77 | badge: 78 | currentVersion() == 'dirty' || latestVersion() != null ? "1" : null, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/screens/components/comic_list_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/screens/components/comic_info_card.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | import 'comic_list.dart'; 5 | import 'content_builder.dart'; 6 | import 'fit_button.dart'; 7 | 8 | class ComicListBuilder extends StatefulWidget { 9 | final Future> future; 10 | final Future Function() reload; 11 | 12 | const ComicListBuilder(this.future, this.reload, {Key? key}) 13 | : super(key: key); 14 | 15 | @override 16 | State createState() => _ComicListBuilderState(); 17 | } 18 | 19 | class _ComicListBuilderState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | return ContentBuilder( 23 | future: widget.future, 24 | onRefresh: widget.reload, 25 | successBuilder: 26 | (BuildContext context, AsyncSnapshot> snapshot) { 27 | return RefreshIndicator( 28 | onRefresh: widget.reload, 29 | child: ComicList( 30 | snapshot.data!, 31 | appendWidget: FitButton( 32 | onPressed: widget.reload, 33 | text: AppLocalizations.of(context)!.refresh, 34 | ), 35 | ), 36 | ); 37 | }, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/screens/components/comic_tags_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'navigator.dart'; 4 | 5 | // 漫画tag 6 | class ComicTagsCard extends StatelessWidget { 7 | final List tags; 8 | 9 | const ComicTagsCard(this.tags, {Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | var theme = Theme.of(context); 14 | return Container( 15 | padding: const EdgeInsets.only(top: 5, bottom: 5), 16 | decoration: BoxDecoration( 17 | border: Border( 18 | bottom: BorderSide( 19 | color: theme.dividerColor, 20 | ), 21 | ), 22 | ), 23 | child: Wrap( 24 | children: tags.map((e) { 25 | return InkWell( 26 | onTap: () { 27 | // todo 28 | // navPushOrReplace(context, (context) => ComicsScreen(tag: e)); 29 | }, 30 | child: Container( 31 | padding: const EdgeInsets.only( 32 | left: 10, 33 | right: 10, 34 | top: 3, 35 | bottom: 3, 36 | ), 37 | margin: const EdgeInsets.only( 38 | left: 5, 39 | right: 5, 40 | top: 3, 41 | bottom: 3, 42 | ), 43 | decoration: BoxDecoration( 44 | color: Colors.pink.shade100, 45 | border: Border.all( 46 | style: BorderStyle.solid, 47 | color: Colors.pink.shade400, 48 | ), 49 | borderRadius: const BorderRadius.all(Radius.circular(30)), 50 | ), 51 | child: Text( 52 | e, 53 | style: TextStyle( 54 | color: Colors.pink.shade500, 55 | height: 1.4, 56 | ), 57 | strutStyle: const StrutStyle( 58 | height: 1.4, 59 | ), 60 | ), 61 | ), 62 | ); 63 | }).toList(), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/screens/components/content_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'content_error.dart'; 4 | import 'content_loading.dart'; 5 | 6 | class ContentBuilder extends StatelessWidget { 7 | final Future future; 8 | final Future Function() onRefresh; 9 | final AsyncWidgetBuilder successBuilder; 10 | 11 | const ContentBuilder( 12 | {Key? key, 13 | required this.future, 14 | required this.onRefresh, 15 | required this.successBuilder}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return FutureBuilder( 21 | future: future, 22 | builder: (BuildContext context, AsyncSnapshot snapshot) { 23 | if (snapshot.hasError) { 24 | return ContentError( 25 | error: snapshot.error, 26 | stackTrace: snapshot.stackTrace, 27 | onRefresh: onRefresh, 28 | ); 29 | } 30 | if (snapshot.connectionState != ConnectionState.done) { 31 | return ContentLoading(label: AppLocalizations.of(context)!.loading); 32 | } 33 | return successBuilder(context, snapshot); 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/screens/components/content_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContentLoading extends StatelessWidget { 4 | final String label; 5 | 6 | const ContentLoading({Key? key, required this.label}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return LayoutBuilder( 11 | builder: (BuildContext context, BoxConstraints constraints) { 12 | var width = constraints.maxWidth; 13 | var height = constraints.maxHeight; 14 | var min = width < height ? width : height; 15 | var theme = Theme.of(context); 16 | return Center( 17 | child: Column( 18 | children: [ 19 | Expanded(child: Container()), 20 | SizedBox( 21 | width: min / 2, 22 | height: min / 2, 23 | child: CircularProgressIndicator( 24 | color: theme.colorScheme.secondary, 25 | backgroundColor: Colors.grey[100], 26 | ), 27 | ), 28 | Container(height: min / 10), 29 | Text(label, style: TextStyle(fontSize: min / 15)), 30 | Expanded(child: Container()), 31 | ], 32 | ), 33 | ); 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/screens/components/error_types.dart: -------------------------------------------------------------------------------- 1 | const errorTypeNetwork = "NETWORK_ERROR"; 2 | const errorTypePermission = "PERMISSION_ERROR"; 3 | const errorTypeTime = "TIME_ERROR"; 4 | 5 | // 错误的类型, 方便照展示和谐的提示 6 | String errorType(String error) { 7 | // EXCEPTION 8 | if (error.contains("timeout") || 9 | error.contains("tcp connect") || 10 | error.contains("connection refused") || 11 | error.contains("deadline") || 12 | error.contains("connection abort") || 13 | error.contains("certificate") || 14 | error.contains("x509") || 15 | error.contains("ssl")) { 16 | return errorTypeNetwork; 17 | } 18 | if (error.contains("permission denied")) { 19 | return errorTypePermission; 20 | } 21 | if (error.contains("time is not synchronize")) { 22 | return errorTypeTime; 23 | } 24 | return ""; 25 | } 26 | -------------------------------------------------------------------------------- /lib/screens/components/fit_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FitButton extends StatelessWidget { 4 | final void Function() onPressed; 5 | final String text; 6 | 7 | const FitButton({Key? key, required this.onPressed, required this.text}) 8 | : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return LayoutBuilder( 13 | builder: (BuildContext context, BoxConstraints constraints) { 14 | return SizedBox( 15 | width: constraints.maxWidth, 16 | height: constraints.maxHeight, 17 | child: Container( 18 | padding: const EdgeInsets.all(10), 19 | child: MaterialButton( 20 | onPressed: onPressed, 21 | child: Center( 22 | child: Text(text), 23 | ), 24 | ), 25 | ), 26 | ); 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/screens/components/item_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // 非全屏FutureBuilder封装 4 | class ItemBuilder extends StatelessWidget { 5 | final Future future; 6 | final AsyncWidgetBuilder successBuilder; 7 | final Future Function() onRefresh; 8 | final double? loadingHeight; 9 | final double? height; 10 | 11 | const ItemBuilder({ 12 | Key? key, 13 | required this.future, 14 | required this.successBuilder, 15 | required this.onRefresh, 16 | this.height, 17 | this.loadingHeight, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return LayoutBuilder( 23 | builder: (BuildContext context, BoxConstraints constraints) { 24 | var _maxWidth = constraints.maxWidth; 25 | var _loadingHeight = height ?? loadingHeight ?? _maxWidth / 2; 26 | return FutureBuilder( 27 | future: future, 28 | builder: (BuildContext context, AsyncSnapshot snapshot) { 29 | if (snapshot.hasError) { 30 | print("${snapshot.error}"); 31 | print("${snapshot.stackTrace}"); 32 | return InkWell( 33 | onTap: onRefresh, 34 | child: SizedBox( 35 | width: _maxWidth, 36 | height: _loadingHeight, 37 | child: Center( 38 | child: 39 | Icon(Icons.sync_problem, size: _loadingHeight / 1.5), 40 | ), 41 | ), 42 | ); 43 | } 44 | if (snapshot.connectionState != ConnectionState.done) { 45 | return SizedBox( 46 | width: _maxWidth, 47 | height: _loadingHeight, 48 | child: Center( 49 | child: Icon(Icons.sync, size: _loadingHeight / 1.5), 50 | ), 51 | ); 52 | } 53 | return SizedBox( 54 | width: _maxWidth, 55 | height: height, 56 | child: successBuilder(context, snapshot), 57 | ); 58 | }); 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/screens/components/mouse_and_touch_scroll_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | 4 | final mouseAndTouchScrollBehavior = MouseAndTouchScrollBehavior(); 5 | 6 | class MouseAndTouchScrollBehavior extends MaterialScrollBehavior { 7 | @override 8 | Set get dragDevices => { 9 | PointerDeviceKind.touch, 10 | PointerDeviceKind.mouse, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /lib/screens/components/navigator.dart: -------------------------------------------------------------------------------- 1 | /// 导航相关 2 | 3 | import 'dart:async'; 4 | import 'package:flutter/material.dart'; 5 | 6 | // 用于监听返回到当前页面的事件 7 | // (await Navigator.push 会在子页面pushReplacement时结束阻塞) 8 | final RouteObserver> routeObserver = 9 | RouteObserver>(); 10 | 11 | // 路径深度计数 12 | 13 | const _depthMax = 15; 14 | var _depth = 0; 15 | 16 | var navigatorObserver = _NavigatorObserver(); 17 | 18 | class _NavigatorObserver extends NavigatorObserver { 19 | @override 20 | void didPop(Route route, Route? previousRoute) { 21 | _depth--; 22 | print("DEPTH : $_depth"); 23 | super.didPop(route, previousRoute); 24 | } 25 | 26 | @override 27 | void didPush(Route route, Route? previousRoute) { 28 | _depth++; 29 | print("DEPTH : $_depth"); 30 | super.didPush(route, previousRoute); 31 | } 32 | } 33 | 34 | // 路径达到一定深度的时候使用 pushReplacement 35 | Future navPushOrReplace( 36 | BuildContext context, WidgetBuilder builder) async { 37 | if (_depth < _depthMax) { 38 | return Navigator.push( 39 | context, 40 | MaterialPageRoute(builder: builder), 41 | ); 42 | } else { 43 | return Navigator.pushReplacement( 44 | context, 45 | MaterialPageRoute(builder: builder), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/screens/file_photo_view_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:html/cross.dart'; 5 | import 'package:photo_view/photo_view.dart'; 6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 7 | import '../commons.dart'; 8 | 9 | // 预览图片 10 | class FilePhotoViewScreen extends StatelessWidget { 11 | final String filePath; 12 | 13 | const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) => Scaffold( 17 | body: Stack( 18 | children: [ 19 | GestureDetector( 20 | onLongPress: () async { 21 | int? choose = await chooseMapDialog( 22 | context, 23 | title: AppLocalizations.of(context)!.choose, 24 | values: { 25 | AppLocalizations.of(context)!.saveImage: 1 26 | }, 27 | ); 28 | switch (choose) { 29 | case 1: 30 | cross.saveImageFileToGallery(filePath, context); 31 | break; 32 | } 33 | }, 34 | child: PhotoView( 35 | imageProvider: FileImage(File(filePath)), 36 | ), 37 | ), 38 | InkWell( 39 | onTap: () => Navigator.of(context).pop(), 40 | child: Container( 41 | margin: const EdgeInsets.only(top: 30), 42 | padding: const EdgeInsets.only(left: 4, right: 4), 43 | decoration: BoxDecoration( 44 | color: Colors.black.withOpacity(.75), 45 | borderRadius: const BorderRadius.only( 46 | topRight: Radius.circular(8), 47 | bottomRight: Radius.circular(8), 48 | ), 49 | ), 50 | child: 51 | const Icon(Icons.keyboard_backspace, color: Colors.white), 52 | ), 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/screens/init_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../configs/export_rename.dart'; 4 | import '../configs/platform.dart'; 5 | import '../configs/proxy.dart'; 6 | import '../configs/android_display_mode.dart'; 7 | import '../configs/android_secure_flag.dart'; 8 | import '../configs/auto_clean.dart'; 9 | import '../configs/auto_full_screen.dart'; 10 | import '../configs/content_failed_reload_action.dart'; 11 | import '../configs/full_screen_action.dart'; 12 | import '../configs/keyboard_controller.dart'; 13 | import '../configs/list_layout.dart'; 14 | import '../configs/no_animation.dart'; 15 | import '../configs/pager_action.dart'; 16 | import '../configs/reader_direction.dart'; 17 | import '../configs/reader_slider_position.dart'; 18 | import '../configs/reader_type.dart'; 19 | import '../configs/themes.dart'; 20 | import '../configs/time_offset_hour.dart'; 21 | import '../configs/version.dart'; 22 | import '../configs/volume_controller.dart'; 23 | import '../cross.dart'; 24 | import '../ffi.dart'; 25 | import 'comics_screen.dart'; 26 | 27 | // 初始化界面 28 | class InitScreen extends StatefulWidget { 29 | const InitScreen({Key? key}) : super(key: key); 30 | 31 | @override 32 | State createState() => _InitScreenState(); 33 | } 34 | 35 | class _InitScreenState extends State { 36 | @override 37 | initState() { 38 | _init(); 39 | super.initState(); 40 | } 41 | 42 | Future _init() async { 43 | // 初始化配置文件 44 | await native.init(root: await cross.root(context)); 45 | await initPlatform(); // 必须第一个初始化, 加载设备信息 46 | await initAutoClean(); 47 | await initProxy(); 48 | await initFont(); 49 | await initTheme(); 50 | await initListLayout(); 51 | await initReaderType(); 52 | await initReaderDirection(); 53 | await initReaderSliderPosition(); 54 | await initAutoFullScreen(); 55 | await initFullScreenAction(); 56 | await initPagerAction(); 57 | await initContentFailedReloadAction(); 58 | await initVolumeController(); 59 | await initKeyboardController(); 60 | await initAndroidDisplayMode(); 61 | await initTimeZone(); 62 | await initAndroidSecureFlag(); 63 | await initNoAnimation(); 64 | await initExportRename(); 65 | await initVersion(); 66 | autoCheckNewVersion(); 67 | Navigator.pushReplacement( 68 | context, 69 | MaterialPageRoute(builder: (context) => const ComicsScreen()), 70 | ); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return Scaffold( 76 | backgroundColor: const Color(0xff8abedc), 77 | body: ConstrainedBox( 78 | constraints: const BoxConstraints.expand(), 79 | child: Image.asset( 80 | "lib/assets/init.png", 81 | fit: BoxFit.contain, 82 | ), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/screens/settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/configs/proxy.dart'; 3 | import 'package:html/screens/about_screen.dart'; 4 | import 'package:html/screens/components/badge.dart'; 5 | import 'package:html/screens/theme_screen.dart'; 6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 7 | import '../configs/themes.dart'; 8 | import '../configs/android_secure_flag.dart'; 9 | import '../configs/auto_clean.dart'; 10 | import '../configs/auto_full_screen.dart'; 11 | import '../configs/content_failed_reload_action.dart'; 12 | import '../configs/full_screen_action.dart'; 13 | import '../configs/keyboard_controller.dart'; 14 | import '../configs/no_animation.dart'; 15 | import '../configs/pager_action.dart'; 16 | import '../configs/reader_direction.dart'; 17 | import '../configs/reader_slider_position.dart'; 18 | import '../configs/reader_type.dart'; 19 | import '../configs/time_offset_hour.dart'; 20 | import '../configs/volume_controller.dart'; 21 | import '../configs/android_display_mode.dart'; 22 | 23 | class SettingsScreen extends StatelessWidget { 24 | const SettingsScreen({Key? key}) : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: Text(AppLocalizations.of(context)!.settings), 31 | ), 32 | body: ListView(children: [ 33 | const Divider(), 34 | ListTile( 35 | onTap: () async { 36 | Navigator.push( 37 | context, 38 | MaterialPageRoute(builder: (context) => const AboutScreen()), 39 | ); 40 | }, 41 | title: VersionBadged( 42 | child: Text(AppLocalizations.of(context)!.about), 43 | ), 44 | ), 45 | const Divider(), 46 | ListTile( 47 | onTap: () async { 48 | if (androidNightModeDisplay) { 49 | Navigator.push( 50 | context, 51 | MaterialPageRoute(builder: (context) => const ThemeScreen()), 52 | ); 53 | } else { 54 | chooseLightTheme(context); 55 | } 56 | }, 57 | title: Text(AppLocalizations.of(context)!.theme), 58 | ), 59 | const Divider(), 60 | proxySetting(), 61 | const Divider(), 62 | const Divider(), 63 | pagerActionSetting(), 64 | contentFailedReloadActionSetting(), 65 | timeZoneSetting(), 66 | const Divider(), 67 | readerTypeSetting(), 68 | readerDirectionSetting(), 69 | readerSliderPositionSetting(), 70 | autoFullScreenSetting(), 71 | fullScreenActionSetting(), 72 | volumeControllerSetting(), 73 | keyboardControllerSetting(), 74 | noAnimationSetting(), 75 | const Divider(), 76 | const Divider(), 77 | autoCleanSecSetting(), 78 | ListTile( 79 | onTap: () { 80 | // todo 81 | }, 82 | title: Text(AppLocalizations.of(context)!.clearCache), 83 | ), 84 | const Divider(), 85 | const Divider(), 86 | androidDisplayModeSetting(), 87 | androidSecureFlagSetting(), 88 | const Divider(), 89 | fontSetting(), 90 | const Divider(), 91 | ]), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/screens/theme_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../configs/themes.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | class ThemeScreen extends StatefulWidget { 6 | const ThemeScreen({Key? key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _ThemeScreenState(); 10 | } 11 | 12 | class _ThemeScreenState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar(title: Text(AppLocalizations.of(context)!.theme)), 17 | body: ListView( 18 | children: [ 19 | const Divider(), 20 | ListTile( 21 | onTap: () async { 22 | await chooseLightTheme(context); 23 | setState(() {}); 24 | }, 25 | title: Text(AppLocalizations.of(context)!.theme), 26 | subtitle: Text(currentLightThemeName()), 27 | ), 28 | const Divider(), 29 | ...androidNightModeDisplay 30 | ? [ 31 | SwitchListTile( 32 | title: Text(AppLocalizations.of(context)!.enableDarkMode), 33 | value: androidNightMode, 34 | onChanged: (value) async { 35 | await setAndroidNightMode(value); 36 | setState(() {}); 37 | }), 38 | ] 39 | : [], 40 | const Divider(), 41 | ...androidNightModeDisplay && androidNightMode 42 | ? [ 43 | ListTile( 44 | onTap: () async { 45 | await chooseDarkTheme(context); 46 | setState(() {}); 47 | }, 48 | title: Text(AppLocalizations.of(context)!.themeDark), 49 | subtitle: Text(currentDarkThemeName()), 50 | ), 51 | ] 52 | : [], 53 | const Divider(), 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(runner LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "html") 5 | set(APPLICATION_ID "niuhuan.html") 6 | 7 | cmake_policy(SET CMP0063 NEW) 8 | 9 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 10 | 11 | # Root filesystem for cross-building. 12 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 13 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 14 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 16 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 17 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 18 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 19 | endif() 20 | 21 | # Configure build options. 22 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 23 | set(CMAKE_BUILD_TYPE "Debug" CACHE 24 | STRING "Flutter build mode" FORCE) 25 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 26 | "Debug" "Profile" "Release") 27 | endif() 28 | 29 | # Compilation settings that should be applied to most targets. 30 | function(APPLY_STANDARD_SETTINGS TARGET) 31 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 32 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 33 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 34 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 35 | endfunction() 36 | 37 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 38 | 39 | # Flutter library and tool build rules. 40 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 41 | 42 | # System-level dependencies. 43 | find_package(PkgConfig REQUIRED) 44 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 45 | 46 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 47 | 48 | # Application build 49 | add_executable(${BINARY_NAME} 50 | "main.cc" 51 | "my_application.cc" 52 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 53 | ) 54 | apply_standard_settings(${BINARY_NAME}) 55 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 56 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 57 | add_dependencies(${BINARY_NAME} flutter_assemble) 58 | # Only the install-generated bundle's copy of the executable will launch 59 | # correctly, since the resources must in the right relative locations. To avoid 60 | # people trying to run the unbundled copy, put it in a subdirectory instead of 61 | # the default top-level location. 62 | set_target_properties(${BINARY_NAME} 63 | PROPERTIES 64 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 65 | ) 66 | 67 | # Generated plugin build rules, which manage building the plugins and adding 68 | # them to the application. 69 | include(flutter/generated_plugins.cmake) 70 | 71 | include(./rust.cmake) 72 | 73 | # === Installation === 74 | # By default, "installing" just makes a relocatable bundle in the build 75 | # directory. 76 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 77 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 78 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 79 | endif() 80 | 81 | # Start with a clean build bundle directory every time. 82 | install(CODE " 83 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 84 | " COMPONENT Runtime) 85 | 86 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 87 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 88 | 89 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 90 | COMPONENT Runtime) 91 | 92 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 93 | COMPONENT Runtime) 94 | 95 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 96 | COMPONENT Runtime) 97 | 98 | if(PLUGIN_BUNDLED_LIBRARIES) 99 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 100 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 101 | COMPONENT Runtime) 102 | endif() 103 | 104 | # Fully re-copy the assets directory on each build to avoid having stale files 105 | # from a previous install. 106 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 107 | install(CODE " 108 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 109 | " COMPONENT Runtime) 110 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 111 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 112 | 113 | # Install the AOT library on non-Debug builds only. 114 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 115 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 116 | COMPONENT Runtime) 117 | endif() 118 | -------------------------------------------------------------------------------- /linux/appimage/AppRun.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Terminal=false 5 | Name=html 6 | Exec=AppRun %u 7 | Icon=AppRun 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /linux/appimage/AppRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/linux/appimage/AppRun.png -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | 11 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 12 | # which isn't available in 3.10. 13 | function(list_prepend LIST_NAME PREFIX) 14 | set(NEW_LIST "") 15 | foreach(element ${${LIST_NAME}}) 16 | list(APPEND NEW_LIST "${PREFIX}${element}") 17 | endforeach(element) 18 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 19 | endfunction() 20 | 21 | # === Flutter Library === 22 | # System-level dependencies. 23 | find_package(PkgConfig REQUIRED) 24 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 25 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 26 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 27 | 28 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 29 | 30 | # Published to parent scope for install step. 31 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 32 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 33 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 34 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 35 | 36 | list(APPEND FLUTTER_LIBRARY_HEADERS 37 | "fl_basic_message_channel.h" 38 | "fl_binary_codec.h" 39 | "fl_binary_messenger.h" 40 | "fl_dart_project.h" 41 | "fl_engine.h" 42 | "fl_json_message_codec.h" 43 | "fl_json_method_codec.h" 44 | "fl_message_codec.h" 45 | "fl_method_call.h" 46 | "fl_method_channel.h" 47 | "fl_method_codec.h" 48 | "fl_method_response.h" 49 | "fl_plugin_registrar.h" 50 | "fl_plugin_registry.h" 51 | "fl_standard_message_codec.h" 52 | "fl_standard_method_codec.h" 53 | "fl_string_codec.h" 54 | "fl_value.h" 55 | "fl_view.h" 56 | "flutter_linux.h" 57 | ) 58 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 59 | add_library(flutter INTERFACE) 60 | target_include_directories(flutter INTERFACE 61 | "${EPHEMERAL_DIR}" 62 | ) 63 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 64 | target_link_libraries(flutter INTERFACE 65 | PkgConfig::GTK 66 | PkgConfig::GLIB 67 | PkgConfig::GIO 68 | ) 69 | add_dependencies(flutter flutter_assemble) 70 | 71 | # === Flutter tool backend === 72 | # _phony_ is a non-existent file to force this command to run every time, 73 | # since currently there's no way to get a full input/output list from the 74 | # flutter tool. 75 | add_custom_command( 76 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 77 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 78 | COMMAND ${CMAKE_COMMAND} -E env 79 | ${FLUTTER_TOOL_ENVIRONMENT} 80 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 81 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 82 | VERBATIM 83 | ) 84 | add_custom_target(flutter_assemble DEPENDS 85 | "${FLUTTER_LIBRARY}" 86 | ${FLUTTER_LIBRARY_HEADERS} 87 | ) 88 | -------------------------------------------------------------------------------- /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 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /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.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "html"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "html"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /linux/rust.cmake: -------------------------------------------------------------------------------- 1 | # We include Corrosion inline here, but ideally in a project with 2 | # many dependencies we would need to install Corrosion on the system. 3 | # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install 4 | # Once done, uncomment this line: 5 | # find_package(Corrosion REQUIRED) 6 | 7 | include(FetchContent) 8 | 9 | FetchContent_Declare( 10 | Corrosion 11 | GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git 12 | GIT_TAG origin/master # Optionally specify a version tag or branch here 13 | ) 14 | 15 | FetchContent_MakeAvailable(Corrosion) 16 | 17 | corrosion_import_crate(MANIFEST_PATH ../native/Cargo.toml) 18 | 19 | # Flutter-specific 20 | 21 | set(CRATE_NAME "native") 22 | 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME}) 24 | 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 26 | -------------------------------------------------------------------------------- /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 url_launcher_macos 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.11' 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 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | dummy_method_to_enforce_bundling() 8 | return true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/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 = html 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = niuhuan.html 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 niuhuan. 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.init() 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.network.client 6 | 7 | com.apple.security.app-sandbox 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "staticlib"] 10 | 11 | [dependencies] 12 | anyhow = "1.0" 13 | async_once = "0.2" 14 | base64 = "0.13" 15 | bytes = "1.1" 16 | chrono = "0.4" 17 | clipboard = "0.5.0" 18 | flutter_rust_bridge = { path = "../../flutter_rust_bridge/frb_rust" } 19 | hex = "0.4" 20 | image = { version = "0", features = ["jpeg", "gif", "webp", "bmp", "png", "jpeg_rayon"] } 21 | itertools = "0.10.3" 22 | lazy_static = "1" 23 | libc = "0.2" 24 | md5 = "0.7" 25 | once_cell = "1" 26 | prost = "0.9" 27 | prost-types = "0.9" 28 | regex = "1.5.5" 29 | reqwest = { version = "0.11", features = ["socks"] } 30 | rsa = "0.5" 31 | rust-crypto = "0" 32 | scraper = "0.13.0" 33 | sea-orm = { version = "0.6", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"], default-features = false } 34 | serde = "1.0" 35 | serde_derive = "1.0" 36 | serde_json = "1.0" 37 | serde_path_to_error = "0.1.7" 38 | tokio = { version = "1", features = ["full"] } 39 | 40 | [target.'cfg(any(target_os = "ios", target_os = "android", target_os = "macos"))'.dependencies] 41 | openssl = { version = "0.10", features = ["vendored"] } 42 | -------------------------------------------------------------------------------- /native/native.xcodeproj/xcuserdata/niuhuan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | native-cdylib.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | native-staticlib.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /native/native.xcodeproj/xcuserdata/vdinh.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | native-cdylib.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | native-staticlib.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /native/src/database/active/comic_view_log.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::QueryOrder; 5 | use sea_orm::QuerySelect; 6 | use sea_orm::{EntityTrait, IntoActiveModel, Set}; 7 | 8 | use crate::database::active::ACTIVE_DATABASE; 9 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 12 | #[sea_orm(table_name = "comic_view_log")] 13 | pub struct Model { 14 | #[sea_orm(primary_key, auto_increment = false)] 15 | pub comic_id: i32, 16 | pub comic_title: String, 17 | pub comic_artists: String, 18 | pub comic_series: String, 19 | pub comic_tags: String, 20 | pub comic_type: String, 21 | pub comic_img1: String, 22 | pub comic_img2: String, 23 | pub add_timestamp_utc: i64, 24 | pub page_rank: i32, 25 | pub view_time: i64, 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 db = ACTIVE_DATABASE.get().unwrap().lock().await; 35 | create_table_if_not_exists(db.deref(), Entity).await; 36 | if !index_exists(db.deref(), "comic_view_log", "comic_view_log_idx_view_time").await { 37 | create_index( 38 | db.deref(), 39 | "comic_view_log", 40 | vec!["view_time"], 41 | "comic_view_log_idx_view_time", 42 | ) 43 | .await; 44 | } 45 | } 46 | 47 | pub(crate) async fn view_info(mut model: Model) -> anyhow::Result<()> { 48 | let db = ACTIVE_DATABASE.get().unwrap().lock().await; 49 | if let Some(in_db) = Entity::find_by_id(model.comic_id.clone()) 50 | .one(db.deref()) 51 | .await? 52 | { 53 | let mut in_db = in_db.into_active_model(); 54 | in_db.comic_id = Set(model.comic_id); 55 | in_db.comic_title = Set(model.comic_title); 56 | in_db.comic_artists = Set(model.comic_artists); 57 | in_db.comic_series = Set(model.comic_series); 58 | in_db.comic_tags = Set(model.comic_tags); 59 | in_db.comic_type = Set(model.comic_type); 60 | in_db.comic_img1 = Set(model.comic_img1); 61 | in_db.comic_img2 = Set(model.comic_img2); 62 | in_db.add_timestamp_utc = Set(model.add_timestamp_utc); 63 | in_db.view_time = Set(chrono::Local::now().timestamp_millis()); 64 | in_db.update(db.deref()).await?; 65 | } else { 66 | model.view_time = chrono::Local::now().timestamp_millis(); 67 | model.into_active_model().insert(db.deref()).await?; 68 | } 69 | Ok(()) 70 | } 71 | 72 | pub(crate) async fn view_page(comic_id: i32, page_rank: i32) -> anyhow::Result<()> { 73 | let db = ACTIVE_DATABASE.get().unwrap().lock().await; 74 | if let Some(in_db) = Entity::find_by_id(comic_id).one(db.deref()).await? { 75 | let mut in_db = in_db.into_active_model(); 76 | in_db.page_rank = Set(page_rank); 77 | in_db.view_time = Set(chrono::Local::now().timestamp_millis()); 78 | in_db.update(db.deref()).await?; 79 | } 80 | Ok(()) 81 | } 82 | 83 | pub(crate) async fn load_view_logs(page: i64) -> anyhow::Result> { 84 | let db = ACTIVE_DATABASE.get().unwrap().lock().await; 85 | Ok(Entity::find() 86 | .order_by_desc(Column::ViewTime) 87 | .offset(page as u64 * 20) 88 | .limit(20) 89 | .all(db.deref()) 90 | .await?) 91 | } 92 | 93 | pub(crate) async fn view_log_by_comic_id(comic_id: i32) -> anyhow::Result> { 94 | let db = ACTIVE_DATABASE.get().unwrap().lock().await; 95 | Ok(Entity::find_by_id(comic_id).one(db.deref()).await?) 96 | } 97 | -------------------------------------------------------------------------------- /native/src/database/active/mod.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use sea_orm::DatabaseConnection; 3 | use tokio::sync::Mutex; 4 | 5 | use crate::database::connect_db; 6 | 7 | pub(crate) mod comic_view_log; 8 | 9 | pub(crate) static ACTIVE_DATABASE: OnceCell> = OnceCell::new(); 10 | 11 | pub(crate) async fn init() { 12 | let db = connect_db("active.db").await; 13 | ACTIVE_DATABASE.set(Mutex::new(db)).unwrap(); 14 | // init tables 15 | comic_view_log::init().await; 16 | } 17 | -------------------------------------------------------------------------------- /native/src/database/cache/image_cache.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 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 | 10 | use crate::database::cache::CACHE_DATABASE; 11 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 12 | 13 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 14 | #[sea_orm(table_name = "image_cache")] 15 | pub struct Model { 16 | #[sea_orm(primary_key, auto_increment = false)] 17 | pub image_key: String, 18 | pub useful: String, 19 | pub extends_field_int_first: Option, 20 | pub extends_field_int_second: Option, 21 | pub extends_field_int_third: Option, 22 | pub extends_field_int_fourth: Option, 23 | pub extends_field_int_fifth: Option, 24 | pub extends_field_string_first: Option, 25 | pub extends_field_string_second: Option, 26 | pub extends_field_string_third: Option, 27 | pub extends_field_string_fourth: Option, 28 | pub extends_field_string_fifth: Option, 29 | pub local_path: String, 30 | pub cache_time: i64, 31 | } 32 | 33 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 34 | pub enum Relation {} 35 | 36 | impl ActiveModelBehavior for ActiveModel {} 37 | 38 | pub(crate) async fn init() { 39 | let gdb = CACHE_DATABASE.get().unwrap().lock().await; 40 | let db = gdb.deref(); 41 | create_table_if_not_exists(db, Entity).await; 42 | if !index_exists(db, "image_cache", "image_cache_idx_cache_time").await { 43 | create_index( 44 | db, 45 | "image_cache", 46 | vec!["cache_time"], 47 | "image_cache_idx_cache_time", 48 | ) 49 | .await; 50 | } 51 | } 52 | 53 | pub(crate) async fn load_image_by_image_key(image_key: String) -> anyhow::Result> { 54 | Ok(Entity::find_by_id(image_key) 55 | .one(CACHE_DATABASE.get().unwrap().lock().await.deref()) 56 | .await?) 57 | } 58 | 59 | pub(crate) async fn load_images_by_image_keys( 60 | image_keys: Vec, 61 | ) -> anyhow::Result> { 62 | Ok(Entity::find() 63 | .filter(Column::ImageKey.is_in(image_keys)) 64 | .all(CACHE_DATABASE.get().unwrap().lock().await.deref()) 65 | .await?) 66 | } 67 | 68 | pub(crate) async fn insert(model: Model) -> anyhow::Result { 69 | Ok(model 70 | .into_active_model() 71 | .insert(CACHE_DATABASE.get().unwrap().lock().await.deref()) 72 | .await?) 73 | } 74 | 75 | pub(crate) async fn update_cache_time(image_key: String) -> anyhow::Result<()> { 76 | Entity::update_many() 77 | .col_expr( 78 | Column::CacheTime, 79 | Expr::value(chrono::Local::now().timestamp_millis()), 80 | ) 81 | .filter(Column::ImageKey.eq(image_key)) 82 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 83 | .await?; 84 | Ok(()) 85 | } 86 | 87 | pub(crate) async fn take_100_cache(time: i64) -> anyhow::Result> { 88 | Ok(Entity::find() 89 | .filter(Column::CacheTime.lt(time)) 90 | .order_by_asc(Column::CacheTime) 91 | .limit(100) 92 | .all(CACHE_DATABASE.get().unwrap().lock().await.deref()) 93 | .await?) 94 | } 95 | 96 | pub(crate) async fn delete_by_image_key(image_key: String) -> anyhow::Result<()> { 97 | Entity::delete_many() 98 | .filter(Column::ImageKey.eq(image_key)) 99 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 100 | .await?; 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /native/src/database/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use sea_orm::{ConnectionTrait, DatabaseConnection, ExecResult, Statement}; 3 | use tokio::sync::Mutex; 4 | 5 | use crate::database::connect_db; 6 | 7 | pub(crate) mod image_cache; 8 | pub(crate) mod web_cache; 9 | 10 | pub(crate) static CACHE_DATABASE: OnceCell> = OnceCell::new(); 11 | 12 | pub(crate) async fn init() { 13 | let db = connect_db("cache.db").await; 14 | CACHE_DATABASE.set(Mutex::new(db)).unwrap(); 15 | // init tables 16 | image_cache::init().await; 17 | web_cache::init().await; 18 | } 19 | 20 | pub(crate) async fn vacuum() -> anyhow::Result<()> { 21 | let db = CACHE_DATABASE.get().unwrap().lock().await; 22 | let backend = db.get_database_backend(); 23 | let _: ExecResult = db 24 | .execute(Statement::from_string(backend, "VACUUM".to_owned())) 25 | .await?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /native/src/database/cache/web_cache.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::ops::Deref; 3 | use std::pin::Pin; 4 | use std::time::Duration; 5 | 6 | use sea_orm::entity::prelude::*; 7 | use sea_orm::sea_query::Expr; 8 | use sea_orm::EntityTrait; 9 | use sea_orm::IntoActiveModel; 10 | 11 | use crate::database::cache::CACHE_DATABASE; 12 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 13 | 14 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 15 | #[sea_orm(table_name = "web_cache")] 16 | pub struct Model { 17 | #[sea_orm(primary_key, auto_increment = false)] 18 | pub cache_key: String, 19 | pub cache_content: String, 20 | pub cache_time: 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 gdb = CACHE_DATABASE.get().unwrap().lock().await; 30 | let db = gdb.deref(); 31 | create_table_if_not_exists(db, Entity).await; 32 | if !index_exists(db, "web_cache", "web_cache_idx_cache_time").await { 33 | create_index( 34 | db, 35 | "web_cache", 36 | vec!["cache_time"], 37 | "web_cache_idx_cache_time", 38 | ) 39 | .await; 40 | } 41 | } 42 | 43 | pub(crate) async fn cache_first serde::Deserialize<'de> + serde::Serialize>( 44 | key: String, 45 | expire: Duration, 46 | pin: Pin>>>, 47 | ) -> anyhow::Result { 48 | let time = chrono::Local::now().timestamp_millis(); 49 | let db = CACHE_DATABASE.get().unwrap().lock().await; 50 | let in_db = Entity::find_by_id(key.clone()).one(db.deref()).await?; 51 | if let Some(ref model) = in_db { 52 | if time < (model.cache_time + expire.as_millis() as i64) { 53 | return Ok(serde_json::from_str(&model.cache_content)?); 54 | } 55 | }; 56 | drop(db); 57 | let t = pin.await?; 58 | let content = serde_json::to_string(&t)?; 59 | let db = CACHE_DATABASE.get().unwrap().lock().await; 60 | let in_db = Entity::find_by_id(key.clone()).one(db.deref()).await?; 61 | if let Some(_) = in_db { 62 | Entity::update_many() 63 | .filter(Column::CacheKey.eq(key.clone())) 64 | .col_expr(Column::CacheTime, Expr::value(time.clone())) 65 | .col_expr(Column::CacheContent, Expr::value(content.clone())) 66 | .exec(db.deref()) 67 | .await?; 68 | } else { 69 | Model { 70 | cache_key: key, 71 | cache_content: content, 72 | cache_time: time, 73 | } 74 | .into_active_model() 75 | .insert(db.deref()) 76 | .await?; 77 | } 78 | Ok(t) 79 | } 80 | 81 | pub(crate) async fn clean_web_cache_by_time(time: i64) -> anyhow::Result<()> { 82 | Entity::delete_many() 83 | .filter(Column::CacheTime.lt(time)) 84 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 85 | .await?; 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /native/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use sea_orm::prelude::DatabaseConnection; 4 | use sea_orm::{ConnectionTrait, EntityTrait, Schema, Statement}; 5 | 6 | use crate::{get_database_dir, join_paths}; 7 | 8 | pub(crate) mod active; 9 | pub(crate) mod cache; 10 | pub(crate) mod properties; 11 | 12 | pub(crate) async fn init_database() { 13 | cache::init().await; 14 | properties::init().await; 15 | active::init().await; 16 | } 17 | 18 | pub(crate) async fn connect_db(path: &str) -> DatabaseConnection { 19 | println!("CONNECT TO DB : {}", path); 20 | let path = join_paths(vec![get_database_dir().as_str(), path]); 21 | println!("DB PATH : {}", path); 22 | let url = format!("sqlite:{}?mode=rwc", path); 23 | let mut opt = sea_orm::ConnectOptions::new(url); 24 | opt.max_connections(20) 25 | .min_connections(5) 26 | .connect_timeout(Duration::from_secs(8)) 27 | .idle_timeout(Duration::from_secs(8)) 28 | .sqlx_logging(true); 29 | sea_orm::Database::connect(opt).await.unwrap() 30 | } 31 | 32 | pub(crate) async fn create_table_if_not_exists(db: &DatabaseConnection, entity: E) 33 | where 34 | E: EntityTrait, 35 | { 36 | if !has_table(db, entity.table_name()).await { 37 | create_table(db, entity).await; 38 | }; 39 | } 40 | 41 | pub(crate) async fn has_table(db: &DatabaseConnection, table_name: &str) -> bool { 42 | let stmt = Statement::from_string( 43 | db.get_database_backend(), 44 | format!( 45 | "SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='{}';", 46 | table_name, 47 | ), 48 | ); 49 | let rsp = db.query_one(stmt).await.unwrap().unwrap(); 50 | let count: i32 = rsp.try_get("", "c").unwrap(); 51 | count > 0 52 | } 53 | 54 | pub(crate) async fn create_table(db: &DatabaseConnection, entity: E) 55 | where 56 | E: EntityTrait, 57 | { 58 | let builder = db.get_database_backend(); 59 | let schema = Schema::new(builder); 60 | let stmt = &schema.create_table_from_entity(entity); 61 | let stmt = builder.build(stmt); 62 | db.execute(stmt).await.unwrap(); 63 | } 64 | 65 | pub(crate) async fn index_exists( 66 | db: &DatabaseConnection, 67 | table_name: &str, 68 | index_name: &str, 69 | ) -> bool { 70 | let stmt = Statement::from_string( 71 | db.get_database_backend(), 72 | format!( 73 | "select COUNT(*) AS c from sqlite_master where type='index' AND tbl_name='{}' AND name='{}';", 74 | table_name, index_name, 75 | ), 76 | ); 77 | db.query_one(stmt) 78 | .await 79 | .unwrap() 80 | .unwrap() 81 | .try_get::("", "c") 82 | .unwrap() 83 | > 0 84 | } 85 | 86 | pub(crate) async fn create_index_a( 87 | db: &DatabaseConnection, 88 | table_name: &str, 89 | columns: Vec<&str>, 90 | index_name: &str, 91 | uk: bool, 92 | ) { 93 | let stmt = Statement::from_string( 94 | db.get_database_backend(), 95 | format!( 96 | "CREATE {} INDEX {} ON {}({});", 97 | if uk { "UNIQUE" } else { "" }, 98 | index_name, 99 | table_name, 100 | columns.join(","), 101 | ), 102 | ); 103 | db.execute(stmt).await.unwrap(); 104 | } 105 | 106 | #[allow(dead_code)] 107 | pub(crate) async fn create_index( 108 | db: &DatabaseConnection, 109 | table_name: &str, 110 | columns: Vec<&str>, 111 | index_name: &str, 112 | ) { 113 | create_index_a(db, table_name, columns, index_name, false).await 114 | } 115 | -------------------------------------------------------------------------------- /native/src/database/properties/mod.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use sea_orm::DatabaseConnection; 3 | use tokio::sync::Mutex; 4 | 5 | use crate::database::connect_db; 6 | 7 | pub(crate) mod property; 8 | 9 | pub(crate) static PROPERTIES_DATABASE: OnceCell> = OnceCell::new(); 10 | 11 | pub(crate) async fn init() { 12 | let db = connect_db("properties.db").await; 13 | PROPERTIES_DATABASE.set(Mutex::new(db)).unwrap(); 14 | // init tables 15 | property::init().await; 16 | } 17 | -------------------------------------------------------------------------------- /native/src/database/properties/property.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::IntoActiveModel; 5 | use sea_orm::{EntityTrait, Set}; 6 | 7 | use crate::database::properties::PROPERTIES_DATABASE; 8 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 9 | 10 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 11 | #[sea_orm(table_name = "property")] 12 | pub struct Model { 13 | #[sea_orm(primary_key, auto_increment = false)] 14 | pub k: String, 15 | pub v: String, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 19 | pub enum Relation {} 20 | 21 | impl ActiveModelBehavior for ActiveModel {} 22 | 23 | pub(crate) async fn init() { 24 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 25 | create_table_if_not_exists(db.deref(), Entity).await; 26 | if !index_exists(db.deref(), "property", "property_idx_k").await { 27 | create_index(db.deref(), "property", vec!["k"], "property_idx_k").await; 28 | } 29 | } 30 | 31 | pub async fn save_property(k: String, v: String) -> anyhow::Result<()> { 32 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 33 | if let Some(in_db) = Entity::find_by_id(k.clone()).one(db.deref()).await? { 34 | let mut in_db = in_db.into_active_model(); 35 | in_db.v = Set(v); 36 | in_db.update(db.deref()).await?; 37 | } else { 38 | Model { k, v } 39 | .into_active_model() 40 | .insert(db.deref()) 41 | .await?; 42 | } 43 | Ok(()) 44 | } 45 | 46 | pub async fn load_property(k: String) -> anyhow::Result { 47 | let in_db = Entity::find_by_id(k) 48 | .one(PROPERTIES_DATABASE.get().unwrap().lock().await.deref()) 49 | .await?; 50 | Ok(if let Some(in_db) = in_db { 51 | in_db.v 52 | } else { 53 | "".to_owned() 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /native/src/hitomi_client/gg.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | 3 | use crate::Result; 4 | 5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 6 | pub struct GG { 7 | pub b: String, 8 | pub m_list: Vec, 9 | pub m_result: i64, 10 | } 11 | 12 | impl GG { 13 | pub(crate) fn s(&self, hash: &str) -> Result { 14 | let len = hash.len(); 15 | let s = format!( 16 | "{}{}{}", 17 | &hash[len - 1..len], 18 | &hash[len - 3..len - 2], 19 | &hash[len - 2..len - 1] 20 | ); 21 | Ok(i64::from_str_radix(&s, 16)?) 22 | } 23 | 24 | pub(crate) fn m(&self, s: i64) -> i64 { 25 | if self.m_list.contains(&s) { 26 | self.m_result 27 | } else if self.m_result == 0 { 28 | 1 29 | } else { 30 | 0 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /native/src/hitomi_client/mod.rs: -------------------------------------------------------------------------------- 1 | pub use client::*; 2 | pub use entities::*; 3 | 4 | mod client; 5 | pub mod entities; 6 | pub(crate) mod gg; 7 | #[cfg(test)] 8 | mod tests; 9 | -------------------------------------------------------------------------------- /native/src/hitomi_client/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | 3 | use super::{Client, ComicFile, ComicFilter, ComicFilterType, Lang, SortType}; 4 | 5 | fn print(result: Result) 6 | where 7 | T: serde::Serialize + Send + Sync, 8 | { 9 | match result { 10 | Ok(t) => match serde_json::to_string(&t) { 11 | Ok(text) => println!("{}", text), 12 | Err(err) => panic!("{}", err), 13 | }, 14 | Err(err) => panic!("{}", err), 15 | } 16 | } 17 | 18 | #[cfg(target_os = "windows")] 19 | fn client() -> Client { 20 | crate::Client::new_with_agent( 21 | reqwest::ClientBuilder::new() 22 | .proxy(reqwest::Proxy::all("socks5://127.0.0.1:10808/").unwrap()) 23 | .build() 24 | .unwrap(), 25 | ) 26 | } 27 | 28 | #[tokio::test] 29 | async fn test_comics() { 30 | print( 31 | client() 32 | .comics( 33 | ComicFilter { 34 | filter_type: ComicFilterType::Tag, 35 | filter_value: "full color".to_string(), 36 | }, 37 | SortType::PopularWeek, 38 | Lang::Ja, 39 | 0, 40 | 10, 41 | ) 42 | .await, 43 | ); 44 | // {"records":[2202264,2202105,2202259,2202253,2202254,2202252,2202250,2202251,2202248,2202247],"min_index":0,"max_index":9,"limit":10,"offset":0,"total":705295} 45 | } 46 | 47 | #[tokio::test] 48 | async fn test_comic_introduction() { 49 | print(client().comic_introduction(2202248).await); 50 | } 51 | 52 | #[tokio::test] 53 | async fn test_comic_reader_info() { 54 | print(client().comic_reader_info(2202248).await); 55 | } 56 | 57 | const TEST_FILE: &str = r#"{"name":"4.jpg","width":1204,"hasavif":1,"hash":"b77ef8cdf4461a43f3a58acaffa95abd7aae805ce6a1b1d52479aeb14ed80d93","haswebp":1,"height":1700}"#; 58 | 59 | #[tokio::test] 60 | async fn test_file_url() { 61 | let client = client(); 62 | let gg = client.download_gg().await.unwrap(); 63 | let file: ComicFile = serde_json::from_str(TEST_FILE).unwrap(); 64 | println!("{}", client.file_url(&gg, &file).unwrap()); 65 | // web https://aa.hitomi.la/avif/1650877201/648/e474b251eda29035d7423b527b0e507034d7613b3dd3e7fb910c0f600f144882.avif 66 | // https://ba.hitomi.la/avif/1650873602/985/b77ef8cdf4461a43f3a58acaffa95abd7aae805ce6a1b1d52479aeb14ed80d93.avif 67 | // 2202248 68 | // .header("Referer", "$BASE_URL/reader/$hlId.html") 69 | // curl -x socks5://localhost:10808/ -H "Referer: https://hitomi.la/reader/2202248.html" https://ba.hitomi.la/webp/1650873602/648/e474b251eda29035d7423b527b0e507034d7613b3dd3e7fb910c0f600f144882.webp 70 | } 71 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use anyhow::Result; 4 | use lazy_static::lazy_static; 5 | use once_cell::sync::OnceCell; 6 | use tokio::runtime; 7 | use tokio::sync::RwLock; 8 | 9 | use hitomi_client::Client; 10 | use local::join_paths; 11 | 12 | use crate::database::init_database; 13 | use crate::local::create_dir_if_not_exists; 14 | 15 | mod api; 16 | mod bridge_generated; 17 | mod database; 18 | pub mod hitomi_client; 19 | 20 | mod local; 21 | mod utils; 22 | 23 | #[cfg(test)] 24 | mod tests; 25 | 26 | lazy_static! { 27 | pub(crate) static ref RUNTIME: runtime::Runtime = runtime::Builder::new_multi_thread() 28 | .enable_all() 29 | .thread_keep_alive(tokio::time::Duration::new(60, 0)) 30 | .worker_threads(30) 31 | .max_blocking_threads(30) 32 | .build() 33 | .unwrap(); 34 | pub(crate) static ref CLIENT: Arc> = Arc::new(RwLock::new(Client::new())); 35 | static ref INIT_ED: Mutex = Mutex::new(false); 36 | } 37 | 38 | static ROOT: OnceCell = OnceCell::new(); 39 | static IMAGE_CACHE_DIR: OnceCell = OnceCell::new(); 40 | static DATABASE_DIR: OnceCell = OnceCell::new(); 41 | 42 | pub fn init_root(path: &str) { 43 | let mut lock = INIT_ED.lock().unwrap(); 44 | if *lock { 45 | return; 46 | } 47 | *lock = true; 48 | println!("Init application with root : {}", path); 49 | ROOT.set(path.to_owned()).unwrap(); 50 | IMAGE_CACHE_DIR 51 | .set(join_paths(vec![path, "image_cache"])) 52 | .unwrap(); 53 | DATABASE_DIR 54 | .set(join_paths(vec![path, "database"])) 55 | .unwrap(); 56 | create_dir_if_not_exists(ROOT.get().unwrap()); 57 | create_dir_if_not_exists(IMAGE_CACHE_DIR.get().unwrap()); 58 | create_dir_if_not_exists(DATABASE_DIR.get().unwrap()); 59 | RUNTIME.block_on(init_database()); 60 | } 61 | 62 | #[allow(dead_code)] 63 | pub(crate) fn get_root() -> &'static String { 64 | ROOT.get().unwrap() 65 | } 66 | 67 | pub(crate) fn get_image_cache_dir() -> &'static String { 68 | IMAGE_CACHE_DIR.get().unwrap() 69 | } 70 | 71 | pub(crate) fn get_database_dir() -> &'static String { 72 | DATABASE_DIR.get().unwrap() 73 | } 74 | -------------------------------------------------------------------------------- /native/src/local.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | #[allow(dead_code)] 4 | pub(crate) fn join_paths>(paths: Vec

) -> String { 5 | match paths.len() { 6 | 0 => String::default(), 7 | _ => { 8 | let mut path: PathBuf = PathBuf::new(); 9 | for x in paths { 10 | path = path.join(x); 11 | } 12 | return path.to_str().unwrap().to_string(); 13 | } 14 | } 15 | } 16 | 17 | pub(crate) fn create_dir_if_not_exists(path: &String) { 18 | if !Path::new(path).exists() { 19 | std::fs::create_dir_all(path).unwrap(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /native/src/tests.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/native/src/tests.rs -------------------------------------------------------------------------------- /native/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::DefaultHasher; 2 | use std::hash::Hasher; 3 | 4 | use lazy_static::lazy_static; 5 | use tokio::sync::{Mutex, MutexGuard}; 6 | 7 | lazy_static! { 8 | static ref HASH_LOCK: Vec> = { 9 | let mut mutex_vec: Vec> = vec![]; 10 | for _ in 0..64 { 11 | mutex_vec.push(Mutex::<()>::new(())); 12 | } 13 | mutex_vec 14 | }; 15 | } 16 | 17 | pub(crate) async fn hash_lock(url: &String) -> MutexGuard<'static, ()> { 18 | let mut s = DefaultHasher::new(); 19 | s.write(url.as_bytes()); 20 | HASH_LOCK[s.finish() as usize % HASH_LOCK.len()] 21 | .lock() 22 | .await 23 | } 24 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: html 2 | description: A image gallery client. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=2.16.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | flutter_localizations: 12 | sdk: flutter 13 | intl: ^0.17.0 14 | 15 | cupertino_icons: ^1.0.2 16 | flutter_rust_bridge: 17 | path: ../flutter_rust_bridge/frb_dart 18 | url_launcher: ^6.0.20 19 | flutter_styled_toast: ^2.0.0 20 | waterfall_flow: ^3.0.2 21 | permission_handler: ^9.2.0 22 | photo_view: ^0.13.0 23 | decorated_icon: ^1.2.1 24 | modal_bottom_sheet: ^2.0.1 25 | event: ^2.1.2 26 | flutter_svg: ^1.0.3 27 | another_xlider: ^1.0.1+2 28 | file_picker: ^4.5.1 29 | clipboard: ^0.1.3 30 | scrollable_positioned_list: ^0.2.3 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | flutter_lints: ^1.0.0 36 | 37 | flutter: 38 | generate: true 39 | uses-material-design: true 40 | assets: 41 | - lib/assets/ 42 | -------------------------------------------------------------------------------- /scripts/bind.android.sh: -------------------------------------------------------------------------------- 1 | 2 | cd "$( cd "$( dirname "$0" )" && pwd )/.." 3 | 4 | echo > native/src/bridge_generated.rs 5 | flutter_rust_bridge_codegen --llvm-path "$LLVM_HOME" --rust-input native/src/api.rs --dart-output lib/bridge_generated.dart 6 | 7 | cd native 8 | rm -rf ../android/app/src/main/jniLibs 9 | cargo ndk -o ../android/app/src/main/jniLibs -t arm64-v8a build --release 10 | 11 | cd "$( cd "$( dirname "$0" )" && pwd )/.." 12 | flutter build apk --target-platform android-arm64 13 | -------------------------------------------------------------------------------- /scripts/bind.ios.sh: -------------------------------------------------------------------------------- 1 | 2 | cd "$( cd "$( dirname "$0" )" && pwd )/.." 3 | 4 | touch native/src/bridge_generated.rs 5 | flutter_rust_bridge_codegen \ 6 | --rust-input native/src/api.rs \ 7 | --dart-output lib/bridge_generated.dart \ 8 | --c-output ios/Runner/bridge_generated.h \ 9 | --rust-crate-dir native \ 10 | --llvm-path $LLVM_HOME \ 11 | --class-name Native 12 | cargo build --manifest-path native/Cargo.toml --features= --lib --release --target=aarch64-apple-ios 13 | cp native/target/aarch64-apple-ios/release/libnative.a ios/Runner/ 14 | -------------------------------------------------------------------------------- /scripts/bind.linux.sh: -------------------------------------------------------------------------------- 1 | 2 | cbindgen native/src/platforms/non_android.rs -l c > linux/native.h 3 | 4 | # llvm (see https://pub.dev/packages/ffigen) 5 | # sudo apt-get install libclang-dev 6 | 7 | touch native/src/bridge_generated.rs 8 | flutter_rust_bridge_codegen \ 9 | --rust-input native/src/api.rs \ 10 | --dart-output lib/bridge_generated.dart \ 11 | --c-output linux/bridge_generated.h \ 12 | --rust-crate-dir native \ 13 | --class-name Native 14 | -------------------------------------------------------------------------------- /scripts/bind.macos.sh: -------------------------------------------------------------------------------- 1 | 2 | cd "$( cd "$( dirname "$0" )" && pwd )/.." 3 | 4 | touch native/src/bridge_generated.rs 5 | flutter_rust_bridge_codegen \ 6 | --rust-input native/src/api.rs \ 7 | --dart-output lib/bridge_generated.dart \ 8 | --c-output macos/Runner/bridge_generated.h \ 9 | --rust-crate-dir native \ 10 | --llvm-path "$LLVM_HOME" \ 11 | --class-name Native 12 | -------------------------------------------------------------------------------- /scripts/bind.windows.cmd: -------------------------------------------------------------------------------- 1 | 2 | @echo. > native/src/bridge_generated.rs 3 | 4 | flutter_rust_bridge_codegen --rust-crate-dir native --rust-input native/src/api.rs --dart-output lib/bridge_generated.dart 5 | -------------------------------------------------------------------------------- /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/widget_test.dart: -------------------------------------------------------------------------------- 1 | // import 'package:flutter/material.dart'; 2 | // import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | // Your tests go here. 6 | } 7 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(html LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "html") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | include(./rust.cmake) 54 | 55 | # === Installation === 56 | # Support files are copied into place next to the executable, so that it can 57 | # run in place. This is done instead of making a separate bundle (as on Linux) 58 | # so that building and running from within Visual Studio will work. 59 | set(BUILD_BUNDLE_DIR "$") 60 | # Make the "install" step default, as it's required to run. 61 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 62 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 63 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 64 | endif() 65 | 66 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 67 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 68 | 69 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 70 | COMPONENT Runtime) 71 | 72 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 73 | COMPONENT Runtime) 74 | 75 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 76 | COMPONENT Runtime) 77 | 78 | if(PLUGIN_BUNDLED_LIBRARIES) 79 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 80 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 81 | COMPONENT Runtime) 82 | endif() 83 | 84 | # Fully re-copy the assets directory on each build to avoid having stale files 85 | # from a previous install. 86 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 87 | install(CODE " 88 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 89 | " COMPONENT Runtime) 90 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 91 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 92 | 93 | # Install the AOT library on non-Debug builds only. 94 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 95 | CONFIGURATIONS Profile;Release 96 | COMPONENT Runtime) 97 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /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 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows 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}/windows plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "utils.cpp" 8 | "win32_window.cpp" 9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 10 | "Runner.rc" 11 | "runner.exe.manifest" 12 | ) 13 | apply_standard_settings(${BINARY_NAME}) 14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 17 | add_dependencies(${BINARY_NAME} flutter_assemble) 18 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "niuhuan" "\0" 93 | VALUE "FileDescription", "html" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "html" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 niuhuan. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "html.exe" "\0" 98 | VALUE "ProductName", "html" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /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.CreateAndShow(L"html", 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/html-comic/ebe2b5635c49b00d1d896da0e7a66a800e8f48b3/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /windows/rust.cmake: -------------------------------------------------------------------------------- 1 | # We include Corrosion inline here, but ideally in a project with 2 | # many dependencies we would need to install Corrosion on the system. 3 | # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install 4 | # Once done, uncomment this line: 5 | # find_package(Corrosion REQUIRED) 6 | 7 | include(FetchContent) 8 | 9 | FetchContent_Declare( 10 | Corrosion 11 | GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git 12 | GIT_TAG origin/master # Optionally specify a version tag or branch here 13 | ) 14 | 15 | FetchContent_MakeAvailable(Corrosion) 16 | 17 | corrosion_import_crate(MANIFEST_PATH ../native/Cargo.toml) 18 | 19 | # Flutter-specific 20 | 21 | set(CRATE_NAME "native") 22 | 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME}) 24 | 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 26 | --------------------------------------------------------------------------------