├── .github └── workflows │ └── Release.yml ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── jniLibs │ │ │ └── arm64-v8a │ │ │ │ └── libnative.so │ │ ├── kotlin │ │ │ └── niuhuan │ │ │ │ └── coco │ │ │ │ └── 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.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 ├── st02.png └── st03.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── bridge_generated.h └── build │ └── .last_build_id ├── justfile ├── l10n.yaml ├── lib ├── assets │ ├── startup.png │ └── version.txt ├── bridge_generated.dart ├── commons.dart ├── configs │ ├── auto_clean.dart │ ├── host.dart │ ├── pager_column_number.dart │ ├── pager_controller_mode.dart │ ├── platform.dart │ ├── proxy.dart │ ├── themes.dart │ └── versions.dart ├── cross.dart ├── ffi.dart ├── image_cache_provider.dart ├── l10n │ ├── app_en.arb │ └── app_zh.arb ├── main.dart └── screens │ ├── about_screen.dart │ ├── components │ ├── badge.dart │ ├── browser_bottom_sheet.dart │ ├── content_builder.dart │ ├── content_error.dart │ ├── content_loading.dart │ ├── empty_app_bar.dart │ ├── error_types.dart │ ├── mouse_and_touch_scroll_behavior.dart │ ├── post_pager.dart │ ├── right_click_pop.dart │ └── shadow_icon_button.dart │ ├── downloading_screen.dart │ ├── init_screen.dart │ ├── post_info_screen.dart │ ├── posts_screen.dart │ └── tag_choose_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 ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ ├── Release.entitlements │ └── bridge_generated.h ├── native ├── Cargo.toml ├── native.xcodeproj │ ├── project.pbxproj │ └── xcuserdata │ │ ├── niuhuan.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── vdinh.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── src │ ├── api.rs │ ├── bridge_generated.io.rs │ ├── bridge_generated.rs │ ├── database │ ├── active │ │ ├── dl_post.rs │ │ └── mod.rs │ ├── cache │ │ ├── image_cache.rs │ │ ├── mod.rs │ │ └── web_cache.rs │ ├── mod.rs │ └── properties │ │ ├── mod.rs │ │ └── property.rs │ ├── download.rs │ ├── entities.rs │ ├── lib.rs │ ├── local.rs │ ├── tests.rs │ └── utils.rs ├── pubspec.lock ├── pubspec.yaml ├── scripts ├── before-ipa.sh ├── bind.android.sh ├── bind.ios.sh ├── bind.linux.sh ├── bind.macos.sh ├── bind.windows.cmd ├── build-ipa.sh └── 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 /.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 | 57 | .vscode/ 58 | target/ 59 | .editorconfig 60 | .tool-versions -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COCO 2 | 3 | A illustrations and wallpapers client. 4 | 5 | ## Screen Shoots 6 | 7 | ![](images/st01.png) 8 | 9 | ![](images/st02.png) 10 | 11 | ![](images/st03.png) 12 | 13 | ## Details 14 | 15 | ### Supported Platforms 16 | 17 | | Platform | minimum version | 18 | |:------------:|:---------------:| 19 | | Android | API 30 (10) | 20 | | Macos | 10.15 | 21 | | iOS / iPadOS | 13 | 22 | | Windows | 10 (64bit) | 23 | | Linux | - 64bit | 24 | 25 | ### Download pictures location 26 | 27 | | Platform | Location | 28 | |:------------:|:---------------------------:| 29 | | Android | {USER_HOME}/Downloads/coco/ | 30 | | Desktop | {USER_HOME}/Downloads/coco/ | 31 | | iOS / iPadOS | {PHONE}/coco/downloads/ | 32 | 33 | 34 | -------------------------------------------------------------------------------- /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 33 // flutter.compileSdkVersion 30 | // ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "niuhuan.coco" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 50 | minSdkVersion 19 // flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' 72 | } 73 | -------------------------------------------------------------------------------- /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/jniLibs/arm64-v8a/libnative.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/android/app/src/main/jniLibs/arm64-v8a/libnative.so -------------------------------------------------------------------------------- /android/app/src/main/kotlin/niuhuan/coco/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package niuhuan.coco 2 | 3 | import android.content.ContentValues 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.os.Build 7 | import android.os.Environment 8 | import android.os.Handler 9 | import android.os.Looper 10 | import android.provider.MediaStore 11 | import androidx.annotation.NonNull 12 | import androidx.annotation.RequiresApi 13 | import io.flutter.Log 14 | import io.flutter.embedding.android.FlutterActivity 15 | import io.flutter.embedding.engine.FlutterEngine 16 | import io.flutter.plugin.common.MethodChannel 17 | import java.io.File 18 | import java.util.concurrent.Executors 19 | 20 | class MainActivity : FlutterActivity() { 21 | 22 | private val pool = Executors.newCachedThreadPool { runnable -> 23 | Thread(runnable).also { it.isDaemon = true } 24 | } 25 | private val uiThreadHandler = Handler(Looper.getMainLooper()) 26 | 27 | private val notImplementedToken = Any() 28 | private fun MethodChannel.Result.withCoroutine(exec: () -> Any?) { 29 | pool.submit { 30 | try { 31 | val data = exec() 32 | uiThreadHandler.post { 33 | when (data) { 34 | notImplementedToken -> { 35 | notImplemented() 36 | } 37 | 38 | is Unit, null -> { 39 | success(null) 40 | } 41 | 42 | else -> { 43 | success(data) 44 | } 45 | } 46 | } 47 | } catch (e: Exception) { 48 | Log.e("Method", "Exception", e) 49 | uiThreadHandler.post { 50 | error("", e.message, "") 51 | } 52 | } 53 | 54 | } 55 | } 56 | 57 | @RequiresApi(Build.VERSION_CODES.KITKAT) 58 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 59 | super.configureFlutterEngine(flutterEngine) 60 | // Method Channel 61 | MethodChannel( 62 | flutterEngine.dartExecutor.binaryMessenger, 63 | "cross" 64 | ).setMethodCallHandler { call, result -> 65 | result.withCoroutine { 66 | when (call.method) { 67 | "root" -> context!!.filesDir.absolutePath 68 | "downloads_to" -> defaultCocoDir().absolutePath 69 | "androidGetVersion" -> Build.VERSION.SDK_INT 70 | "saveImageToGallery" -> saveImageToGallery(call.arguments as String) 71 | else -> { 72 | notImplementedToken 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | private fun saveImageToGallery(path: String) { 80 | BitmapFactory.decodeFile(path)?.let { bitmap -> 81 | val contentValues = ContentValues().apply { 82 | put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString()) 83 | put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") 84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one 85 | put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) 86 | put(MediaStore.MediaColumns.IS_PENDING, 1) 87 | } 88 | } 89 | contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) 90 | ?.let { uri -> 91 | contentResolver.openOutputStream(uri)?.use { fos -> 92 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) 93 | } 94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one 95 | contentValues.clear() 96 | contentValues.put(MediaStore.Video.Media.IS_PENDING, 0) 97 | contentResolver.update(uri, contentValues, null, null) 98 | } 99 | } 100 | } 101 | } 102 | 103 | fun storageRoot(): String { 104 | return Environment.getExternalStorageDirectory().absolutePath 105 | } 106 | 107 | private fun downloadsDir(): File { 108 | return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) 109 | ?: throw java.lang.IllegalStateException() 110 | } 111 | 112 | private fun defaultCocoDir(): File { 113 | return File(downloadsDir(), "coco") 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.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 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /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_asset/main.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use serde_derive::Serialize; 3 | use serde_json::Value; 4 | use anyhow::Result; 5 | 6 | const OWNER: &str = "niuhuan"; 7 | const REPO: &str = "coco"; 8 | const UA: &str = "niuhuan coco ci"; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let gh_token = std::env::var("GH_TOKEN")?; 13 | if gh_token.is_empty() { 14 | panic!("Please set GH_TOKEN"); 15 | } 16 | 17 | let target = std::env::var("TARGET")?; 18 | 19 | let vs_code_txt = tokio::fs::read_to_string("version.code.txt").await?; 20 | 21 | let code = vs_code_txt.trim(); 22 | 23 | let release_file_name = match target.as_str() { 24 | "macos" => format!("coco-{}-macos-intel.dmg", code), 25 | "ios" => format!("coco-{}-ios-nosign.ipa", code), 26 | "windows" => format!("coco-{}-windows-x86_64.zip", code), 27 | "linux" => format!("coco-{}-linux-x86_64.AppImage", code), 28 | "android-arm32" => format!("coco-{}-android-arm32.apk", code), 29 | "android-arm64" => format!("coco-{}-android-arm64.apk", code), 30 | "android-x86_64" => format!("coco-{}-android-x86_64.apk", code), 31 | un => panic!("unknown target : {}", un), 32 | }; 33 | 34 | let client = reqwest::ClientBuilder::new().user_agent(UA).build()?; 35 | 36 | let check_response = client.get(format!("https://api.github.com/repos/{}/{}/releases/tags/{}", OWNER, REPO, code)) 37 | .header("Authorization", format!("token {}", gh_token)) 38 | .send().await?; 39 | 40 | match check_response.status().as_u16() { 41 | 200 => (), 42 | 404 => println!("release not exists"), 43 | code => { 44 | let text = check_response.text().await?; 45 | panic!("error for check release : {} : {}", code, text); 46 | } 47 | } 48 | let release: Release = check_response.json().await?; 49 | 50 | let ass_names: Vec = release.assets.iter().map(|a| a.name.clone()).collect(); 51 | println!("::set-output name=skip_build::{}", ass_names.contains(&release_file_name)); 52 | Ok(()) 53 | } 54 | 55 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 56 | pub struct Release { 57 | pub url: String, 58 | pub assets_url: String, 59 | pub upload_url: String, 60 | pub html_url: String, 61 | pub id: i64, 62 | pub author: Author, 63 | pub node_id: String, 64 | pub tag_name: String, 65 | pub target_commitish: String, 66 | pub name: String, 67 | pub draft: bool, 68 | pub prerelease: bool, 69 | pub created_at: String, 70 | pub published_at: String, 71 | pub assets: Vec, 72 | pub tarball_url: String, 73 | pub zipball_url: String, 74 | pub body: String, 75 | } 76 | 77 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 78 | pub struct Author { 79 | pub login: String, 80 | pub id: i64, 81 | pub node_id: String, 82 | pub avatar_url: String, 83 | pub gravatar_id: String, 84 | pub url: String, 85 | pub html_url: String, 86 | pub followers_url: String, 87 | pub following_url: String, 88 | pub gists_url: String, 89 | pub starred_url: String, 90 | pub subscriptions_url: String, 91 | pub organizations_url: String, 92 | pub repos_url: String, 93 | pub events_url: String, 94 | pub received_events_url: String, 95 | #[serde(rename = "type")] 96 | pub type_field: String, 97 | pub site_admin: bool, 98 | } 99 | 100 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 101 | pub struct Asset { 102 | pub url: String, 103 | pub id: i64, 104 | pub node_id: String, 105 | pub name: String, 106 | pub label: Value, 107 | pub uploader: Uploader, 108 | pub content_type: String, 109 | pub state: String, 110 | pub size: i64, 111 | pub download_count: i64, 112 | pub created_at: String, 113 | pub updated_at: String, 114 | pub browser_download_url: String, 115 | } 116 | 117 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 118 | pub struct Uploader { 119 | pub login: String, 120 | pub id: i64, 121 | pub node_id: String, 122 | pub avatar_url: String, 123 | pub gravatar_id: String, 124 | pub url: String, 125 | pub html_url: String, 126 | pub followers_url: String, 127 | pub following_url: String, 128 | pub gists_url: String, 129 | pub starred_url: String, 130 | pub subscriptions_url: String, 131 | pub organizations_url: String, 132 | pub repos_url: String, 133 | pub events_url: String, 134 | pub received_events_url: String, 135 | #[serde(rename = "type")] 136 | pub type_field: String, 137 | pub site_admin: bool, 138 | } 139 | -------------------------------------------------------------------------------- /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 = "coco"; 7 | const UA: &str = "niuhuan coco 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.7 -------------------------------------------------------------------------------- /ci/version.info.txt: -------------------------------------------------------------------------------- 1 | v0.0.7 2 | - ✨Display picture tags 3 | 4 | v0.0.6 5 | - ♻️Downloads to file browser 6 | -------------------------------------------------------------------------------- /images/ic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/images/ic.png -------------------------------------------------------------------------------- /images/st01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/images/st01.png -------------------------------------------------------------------------------- /images/st02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/images/st02.png -------------------------------------------------------------------------------- /images/st03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/images/st03.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 | 11.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, '11.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/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - permission_handler_apple (9.1.0): 7 | - Flutter 8 | - share_plus (0.0.1): 9 | - Flutter 10 | - url_launcher_ios (0.0.1): 11 | - Flutter 12 | 13 | DEPENDENCIES: 14 | - Flutter (from `Flutter`) 15 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) 16 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 17 | - share_plus (from `.symlinks/plugins/share_plus/ios`) 18 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 19 | 20 | EXTERNAL SOURCES: 21 | Flutter: 22 | :path: Flutter 23 | path_provider_foundation: 24 | :path: ".symlinks/plugins/path_provider_foundation/ios" 25 | permission_handler_apple: 26 | :path: ".symlinks/plugins/permission_handler_apple/ios" 27 | share_plus: 28 | :path: ".symlinks/plugins/share_plus/ios" 29 | url_launcher_ios: 30 | :path: ".symlinks/plugins/url_launcher_ios/ios" 31 | 32 | SPEC CHECKSUMS: 33 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 34 | path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 35 | permission_handler_apple: 8f116445eff3c0e7c65ad60f5fef5490aa94b4e4 36 | share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 37 | url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 38 | 39 | PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 40 | 41 | COCOAPODS: 1.12.1 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 documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] 12 | let fromChars = documentDirectory.cString(using: String.Encoding.utf8) 13 | 14 | let applicationSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] 15 | let chars = applicationSupportDirectory.cString(using: String.Encoding.utf8) 16 | 17 | 18 | let controller = self.window.rootViewController as! FlutterViewController 19 | let channel = FlutterMethodChannel.init(name: "cross", binaryMessenger: controller as! FlutterBinaryMessenger) 20 | 21 | channel.setMethodCallHandler { (call, result) in 22 | Thread { 23 | if call.method == "downloads_to" { 24 | let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]; 25 | result(docDir + "/downloads") 26 | } 27 | else if call.method == "root" { 28 | 29 | result(NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]) 30 | 31 | } 32 | else if call.method == "saveImageToGallery"{ 33 | if let args = call.arguments as? String{ 34 | 35 | do { 36 | let fileURL: URL = URL(fileURLWithPath: args) 37 | let imageData = try Data(contentsOf: fileURL) 38 | 39 | if let uiImage = UIImage(data: imageData) { 40 | UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) 41 | result("OK") 42 | }else{ 43 | result(FlutterError(code: "", message: "Error loading image ", details: "")) 44 | } 45 | 46 | } catch { 47 | result(FlutterError(code: "", message: "Error loading image : \(error)", details: "")) 48 | } 49 | 50 | }else{ 51 | result(FlutterError(code: "", message: "params error", details: "")) 52 | } 53 | } 54 | else{ 55 | result(FlutterMethodNotImplemented) 56 | } 57 | }.start() 58 | } 59 | 60 | print("dummy_value=\(dummy_method_to_enforce_bundling())"); 61 | GeneratedPluginRegistrant.register(with: self) 62 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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 | LSSupportsOpeningDocumentsInPlace 6 | 7 | UIFileSharingEnabled 8 | 9 | LSApplicationCategoryType 10 | public.app-category.entertainment 11 | NSPhotoLibraryUsageDescription 12 | Usage images 13 | NSPhotoLibraryAddUsageDescription 14 | Save images 15 | CFBundleDevelopmentRegion 16 | $(DEVELOPMENT_LANGUAGE) 17 | CFBundleDisplayName 18 | coco 19 | CFBundleExecutable 20 | $(EXECUTABLE_NAME) 21 | CFBundleIdentifier 22 | $(PRODUCT_BUNDLE_IDENTIFIER) 23 | CFBundleInfoDictionaryVersion 24 | 6.0 25 | CFBundleName 26 | coco 27 | CFBundlePackageType 28 | APPL 29 | CFBundleShortVersionString 30 | $(FLUTTER_BUILD_NAME) 31 | CFBundleSignature 32 | ???? 33 | CFBundleVersion 34 | $(FLUTTER_BUILD_NUMBER) 35 | LSRequiresIPhoneOS 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | CADisableMinimumFrameDurationOnPhone 57 | 58 | UIApplicationSupportsIndirectInputEvents 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | #import "bridge_generated.h" 3 | -------------------------------------------------------------------------------- /ios/build/.last_build_id: -------------------------------------------------------------------------------- 1 | f23611a675a8c9bc71eb16e8a1108cf8 -------------------------------------------------------------------------------- /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/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/lib/assets/startup.png -------------------------------------------------------------------------------- /lib/assets/version.txt: -------------------------------------------------------------------------------- 1 | v0.0.4 -------------------------------------------------------------------------------- /lib/commons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_styled_toast/flutter_styled_toast.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | /// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容 6 | Future chooseListDialog(BuildContext context, 7 | {required List values, required String title, String? tips}) async { 8 | return showDialog( 9 | context: context, 10 | builder: (BuildContext context) { 11 | return SimpleDialog( 12 | title: Text(title), 13 | children: [ 14 | ...values.map((e) => SimpleDialogOption( 15 | onPressed: () { 16 | Navigator.of(context).pop(e); 17 | }, 18 | child: Text('$e'), 19 | )), 20 | ...tips != null 21 | ? [ 22 | Container( 23 | padding: const EdgeInsets.fromLTRB(15, 5, 15, 15), 24 | child: Text(tips), 25 | ), 26 | ] 27 | : [], 28 | ], 29 | ); 30 | }, 31 | ); 32 | } 33 | 34 | Widget buildLoading({double? width, double? height, Color? color}) { 35 | double? size; 36 | if (width != null && height != null) { 37 | size = width < height ? width : height; 38 | } 39 | return SizedBox( 40 | width: width, 41 | height: height, 42 | child: Center( 43 | child: Icon( 44 | Icons.downloading, 45 | size: size, 46 | color: color ?? Colors.black12, 47 | ), 48 | ), 49 | ); 50 | } 51 | 52 | /// 显示一个toast 53 | void defaultToast(BuildContext context, String title) { 54 | showToast( 55 | title, 56 | context: context, 57 | position: StyledToastPosition.center, 58 | animation: StyledToastAnimation.scale, 59 | reverseAnimation: StyledToastAnimation.fade, 60 | duration: const Duration(seconds: 4), 61 | animDuration: const Duration(seconds: 1), 62 | curve: Curves.elasticOut, 63 | reverseCurve: Curves.linear, 64 | ); 65 | } 66 | 67 | Future chooseMapDialog( 68 | BuildContext buildContext, { 69 | required String title, 70 | required Map values, 71 | }) async { 72 | return await showDialog( 73 | context: buildContext, 74 | builder: (BuildContext context) { 75 | return SimpleDialog( 76 | title: Text(title), 77 | children: values.entries 78 | .map((e) => SimpleDialogOption( 79 | child: Text(e.key), 80 | onPressed: () { 81 | Navigator.of(context).pop(e.value); 82 | }, 83 | )) 84 | .toList(), 85 | ); 86 | }, 87 | ); 88 | } 89 | 90 | var _controller = 91 | TextEditingController.fromValue(const TextEditingValue(text: '')); 92 | 93 | Future displayTextInputDialog(BuildContext context, 94 | {String? title, 95 | String src = "", 96 | String? hint, 97 | String? desc, 98 | bool isPasswd = false}) { 99 | _controller.text = src; 100 | return showDialog( 101 | context: context, 102 | builder: (context) { 103 | return AlertDialog( 104 | title: title == null ? null : Text(title), 105 | content: SingleChildScrollView( 106 | child: ListBody( 107 | children: [ 108 | TextField( 109 | controller: _controller, 110 | decoration: InputDecoration(hintText: hint), 111 | obscureText: isPasswd, 112 | obscuringCharacter: '\u2022', 113 | ), 114 | ...(desc == null 115 | ? [] 116 | : [ 117 | Container( 118 | padding: const EdgeInsets.only(top: 20, bottom: 10), 119 | child: Text( 120 | desc, 121 | style: TextStyle( 122 | fontSize: 12, 123 | color: Theme.of(context) 124 | .textTheme 125 | .bodyText1 126 | ?.color 127 | ?.withOpacity(.5)), 128 | ), 129 | ) 130 | ]), 131 | ], 132 | ), 133 | ), 134 | actions: [ 135 | MaterialButton( 136 | child: Text(AppLocalizations.of(context)!.cancel), 137 | onPressed: () { 138 | Navigator.of(context).pop(); 139 | }, 140 | ), 141 | MaterialButton( 142 | child: Text(AppLocalizations.of(context)!.ok), 143 | onPressed: () { 144 | Navigator.of(context).pop(_controller.text); 145 | }, 146 | ), 147 | ], 148 | ); 149 | }, 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /lib/configs/auto_clean.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/ffi.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | const _propertyName = "auto_clean"; 8 | late String autoClean; 9 | 10 | Map _nameMap(BuildContext context) => { 11 | (3600 * 24).toString(): AppLocalizations.of(context)!.aDay, 12 | (3600 * 24 * 7).toString(): AppLocalizations.of(context)!.aWeek, 13 | (3600 * 24 * 30).toString(): AppLocalizations.of(context)!.aMonth, 14 | (3600 * 24 * 30 * 12).toString(): AppLocalizations.of(context)!.aYear, 15 | }; 16 | 17 | Future initAutoClean() async { 18 | autoClean = await native.loadProperty(k: _propertyName); 19 | if (autoClean == "") { 20 | autoClean = "${(3600 * 24 * 7)}"; 21 | } 22 | await native.autoClean(time: int.parse(autoClean)); 23 | } 24 | 25 | String autoCleanName(BuildContext context) { 26 | return _nameMap(context)[autoClean] ?? "-"; 27 | } 28 | 29 | Future chooseAutoClean(BuildContext context) async { 30 | String? choose = await chooseMapDialog(context, 31 | title: AppLocalizations.of(context)!.chooseAutoCleanPeriod, 32 | values: _nameMap(context).map((key, value) => MapEntry(value, key))); 33 | if (choose != null) { 34 | await native.saveProperty(k: _propertyName, v: choose); 35 | autoClean = choose; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/configs/host.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/commons.dart'; 2 | import 'package:coco/ffi.dart'; 3 | import 'package:event/event.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | 7 | const hosts = [ 8 | "https://konachan.net", 9 | "https://konachan.com", 10 | "https://yande.re", 11 | "https://rule34.xxx", 12 | ]; 13 | 14 | late String host; 15 | const _k = "host"; 16 | 17 | Future initHost() async { 18 | host = await native.loadProperty(k: _k); 19 | if (host == "") { 20 | host = hosts[0]; 21 | } 22 | } 23 | 24 | final hostEvent = Event(); 25 | 26 | Future chooseHost(BuildContext context) async { 27 | final title = AppLocalizations.of(context)!.webSource; 28 | final net = AppLocalizations.of(context)!.switchToNet; 29 | final com = AppLocalizations.of(context)!.switchToCom; 30 | final re = AppLocalizations.of(context)!.switchToRe; 31 | final xxx = AppLocalizations.of(context)!.switchToXxx; 32 | final choose = await chooseListDialog( 33 | context, 34 | title: title, 35 | values: [net, com, re, xxx], 36 | ); 37 | if (choose != null) { 38 | if (net == choose) { 39 | host = hosts[0]; 40 | } 41 | if (com == choose) { 42 | host = hosts[1]; 43 | } 44 | if (re == choose) { 45 | host = hosts[2]; 46 | } 47 | if (xxx == choose) { 48 | host = hosts[3]; 49 | } 50 | await native.saveProperty(k: _k, v: host); 51 | hostEvent.broadcast(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/configs/pager_column_number.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/ffi.dart'; 2 | import 'package:event/event.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | import '../commons.dart'; 7 | 8 | const _propertyName = "pager_column_number"; 9 | late int _pagerColumnNumber; 10 | 11 | int get pagerColumnNumber => _pagerColumnNumber; 12 | final pageColumnEvent = Event(); 13 | 14 | Future initPagerColumnCount() async { 15 | String numStr = await native.loadProperty(k: _propertyName); 16 | if (numStr == "") { 17 | numStr = "2"; 18 | } 19 | _pagerColumnNumber = int.parse(numStr); 20 | } 21 | 22 | Future choosePagerColumnCount(BuildContext context) async { 23 | final choose = await chooseListDialog( 24 | context, 25 | title: AppLocalizations.of(context)!.choosePagerColumnNumber, 26 | values: List.generate(10, (i) => i + 1), 27 | ); 28 | if (choose != null) { 29 | await native.saveProperty(k: _propertyName, v: choose.toString()); 30 | _pagerColumnNumber = choose; 31 | pageColumnEvent.broadcast(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/configs/pager_controller_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/ffi.dart'; 2 | import 'package:event/event.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | import '../commons.dart'; 7 | 8 | const _propertyKey = "pager_controller_mode"; 9 | late PagerControllerMode _value; 10 | final currentPagerControllerModeEvent = Event(); 11 | 12 | PagerControllerMode get currentPagerControllerMode => _value; 13 | 14 | enum PagerControllerMode { 15 | stream, 16 | pager, 17 | } 18 | 19 | Map _nameMap(BuildContext context) => { 20 | PagerControllerMode.stream: AppLocalizations.of(context)!.stream, 21 | PagerControllerMode.pager: AppLocalizations.of(context)!.pager, 22 | }; 23 | 24 | String currentPagerControllerModeName(BuildContext context) => 25 | _nameMap(context)[_value]!; 26 | 27 | Future choosePagerControllerMode(BuildContext context) async { 28 | final target = await chooseMapDialog(context, 29 | title: AppLocalizations.of(context)!.choosePagerMode, 30 | values: _nameMap(context).map((key, value) => MapEntry(value, key))); 31 | if (target != null && target != _value) { 32 | await native.saveProperty(k: _propertyKey, v: "$target"); 33 | _value = target; 34 | currentPagerControllerModeEvent.broadcast(); 35 | } 36 | } 37 | 38 | PagerControllerMode _parse(String string) { 39 | for (var value in PagerControllerMode.values) { 40 | if ("$value" == string) { 41 | return value; 42 | } 43 | } 44 | return PagerControllerMode.stream; 45 | } 46 | 47 | Future initPagerControllerMode() async { 48 | _value = _parse(await native.loadProperty(k: _propertyKey)); 49 | } 50 | -------------------------------------------------------------------------------- /lib/configs/platform.dart: -------------------------------------------------------------------------------- 1 | /// 平台信息 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:coco/cross.dart'; 6 | 7 | int androidVersion = 0; 8 | 9 | Future initPlatform()async{ 10 | if (Platform.isAndroid) { 11 | androidVersion = await cross.androidGetVersion(); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/configs/proxy.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/ffi.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | import '../commons.dart'; 6 | 7 | const _propertyName = "proxy"; 8 | late String proxy; 9 | 10 | Future initProxy() async { 11 | proxy = await native.loadProperty(k: _propertyName); 12 | try { 13 | native.setProxy(url: proxy); 14 | } catch (e, s) { 15 | print("$e\n$s"); 16 | } 17 | } 18 | 19 | Future inputProxy(BuildContext context) async { 20 | String? input = await displayTextInputDialog( 21 | context, 22 | title: AppLocalizations.of(context)!.inputProxy, 23 | hint: "socks5://127.0.0.1:1080/", 24 | src: proxy, 25 | ); 26 | if (input != null) { 27 | try { 28 | await native.setProxy(url: input); 29 | await native.saveProperty(k: _propertyName, v: input); 30 | proxy = input; 31 | } catch (e, s) { 32 | print("$e\n$s"); 33 | defaultToast(context, "$e"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/configs/themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | 5 | final lightTheme = ThemeData.light().copyWith( 6 | brightness: Brightness.light, 7 | colorScheme: ColorScheme.light( 8 | secondary: Colors.pink.shade200, 9 | ), 10 | appBarTheme: AppBarTheme( 11 | systemOverlayStyle: SystemUiOverlayStyle.light, 12 | color: Colors.pink.shade200, 13 | iconTheme: const IconThemeData( 14 | color: Colors.white, 15 | ), 16 | ), 17 | bottomNavigationBarTheme: BottomNavigationBarThemeData( 18 | selectedItemColor: Colors.pink[300], 19 | unselectedItemColor: Colors.grey[500], 20 | ), 21 | dividerColor: Colors.grey.shade200, 22 | primaryColor: Colors.pink.shade200, 23 | textSelectionTheme: TextSelectionThemeData( 24 | cursorColor: Colors.pink.shade200, 25 | selectionColor: Colors.pink.shade300.withAlpha(150), 26 | selectionHandleColor: Colors.pink.shade300.withAlpha(200), 27 | ), 28 | inputDecorationTheme: InputDecorationTheme( 29 | focusedBorder: UnderlineInputBorder( 30 | borderSide: BorderSide(color: Colors.pink.shade200), 31 | ), 32 | ), 33 | ); 34 | 35 | 36 | final darkTheme = ThemeData.dark().copyWith( 37 | brightness: Brightness.light, 38 | colorScheme: ColorScheme.light( 39 | secondary: Colors.pink.shade200, 40 | ), 41 | appBarTheme: const AppBarTheme( 42 | systemOverlayStyle: SystemUiOverlayStyle.light, 43 | color: Color(0xFF1E1E1E), 44 | iconTheme: IconThemeData( 45 | color: Colors.white, 46 | ), 47 | ), 48 | bottomNavigationBarTheme: BottomNavigationBarThemeData( 49 | selectedItemColor: Colors.white, 50 | unselectedItemColor: Colors.grey.shade300, 51 | backgroundColor: Colors.grey.shade900, 52 | ), 53 | primaryColor: Colors.pink.shade200, 54 | textSelectionTheme: TextSelectionThemeData( 55 | cursorColor: Colors.pink.shade200, 56 | selectionColor: Colors.pink.shade300.withAlpha(150), 57 | selectionHandleColor: Colors.pink.shade300.withAlpha(200), 58 | ), 59 | inputDecorationTheme: InputDecorationTheme( 60 | focusedBorder: UnderlineInputBorder( 61 | borderSide: BorderSide(color: Colors.pink.shade200), 62 | ), 63 | ), 64 | ); 65 | -------------------------------------------------------------------------------- /lib/configs/versions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:convert'; 3 | import 'package:event/event.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart' show rootBundle; 6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 7 | import '../commons.dart'; 8 | import '../ffi.dart'; 9 | 10 | 11 | const _versionUrl = 12 | "https://api.github.com/repos/niuhuan/coco/releases/latest"; 13 | const _versionAssets = 'lib/assets/version.txt'; 14 | RegExp _versionExp = RegExp(r"^v\d+\.\d+.\d+$"); 15 | 16 | late String _version; 17 | String? _latestVersion; 18 | String? _latestVersionInfo; 19 | 20 | Future initVersion() async { 21 | // 当前版本 22 | try { 23 | _version = (await rootBundle.loadString(_versionAssets)).trim(); 24 | } catch (e) { 25 | _version = "dirty"; 26 | } 27 | } 28 | 29 | var versionEvent = Event(); 30 | 31 | String currentVersion() { 32 | return _version; 33 | } 34 | 35 | String? get latestVersion => _latestVersion; 36 | 37 | String? latestVersionInfo() { 38 | return _latestVersionInfo; 39 | } 40 | 41 | Future autoCheckNewVersion() { 42 | return _versionCheck(); 43 | } 44 | 45 | Future manualCheckNewVersion(BuildContext context) async { 46 | try { 47 | defaultToast(context, AppLocalizations.of(context)!.checkingNewVersion); 48 | await _versionCheck(); 49 | defaultToast(context, AppLocalizations.of(context)!.success); 50 | } catch (e) { 51 | defaultToast(context, AppLocalizations.of(context)!.failed + " : $e"); 52 | } 53 | } 54 | 55 | bool dirtyVersion() { 56 | return !_versionExp.hasMatch(_version); 57 | } 58 | 59 | // maybe exception 60 | Future _versionCheck() async { 61 | if (_versionExp.hasMatch(_version)) { 62 | var json = jsonDecode(await native.httpGet(url: _versionUrl)); 63 | if (json["name"] != null) { 64 | String latestVersion = (json["name"]); 65 | if (latestVersion != _version) { 66 | _latestVersion = latestVersion; 67 | _latestVersionInfo = json["body"] ?? ""; 68 | } 69 | } 70 | } // else dirtyVersion 71 | versionEvent.broadcast(); 72 | print("$_latestVersion"); 73 | } 74 | -------------------------------------------------------------------------------- /lib/cross.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:permission_handler/permission_handler.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 8 | 9 | import 'commons.dart'; 10 | import 'ffi.dart'; 11 | 12 | const cross = Cross._(); 13 | 14 | class Cross { 15 | const Cross._(); 16 | 17 | static const _channel = MethodChannel("cross"); 18 | 19 | Future downloads() async { 20 | if (Platform.isIOS) { 21 | return await _channel.invokeMethod("downloads_to"); 22 | } else if (Platform.isAndroid) { 23 | return await _channel.invokeMethod("downloads_to"); 24 | } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { 25 | return await native.downloadsTo(); 26 | } 27 | throw "没有适配的平台"; 28 | } 29 | 30 | Future root() async { 31 | if (Platform.isAndroid || Platform.isIOS) { 32 | return await _channel.invokeMethod("root"); 33 | } 34 | if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 35 | return await native.desktopRoot(); 36 | } 37 | throw "没有适配的平台"; 38 | } 39 | 40 | Future androidGetVersion() async { 41 | return await _channel.invokeMethod("androidGetVersion"); 42 | } 43 | // 44 | // Future saveImageToGallery(String path, BuildContext context) async { 45 | // if (Platform.isAndroid) { 46 | // if (!(await Permission.storage.request()).isGranted) { 47 | // return; 48 | // } 49 | // } 50 | // if (Platform.isIOS || Platform.isAndroid) { 51 | // try { 52 | // await _channel.invokeMethod("saveImageToGallery", path); 53 | // defaultToast( 54 | // context, 55 | // AppLocalizations.of(context)!.success, 56 | // ); 57 | // } catch (e) { 58 | // // todo errorToast 59 | // defaultToast( 60 | // context, 61 | // "${AppLocalizations.of(context)!.failed} : $e", 62 | // ); 63 | // } 64 | // } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { 65 | // String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); 66 | // if (selectedDirectory != null) { 67 | // try { 68 | // await native.copyImageTo(srcPath: path, toDir: selectedDirectory); 69 | // defaultToast( 70 | // context, 71 | // AppLocalizations.of(context)!.success, 72 | // ); 73 | // } catch (e) { 74 | // // todo errorToast 75 | // defaultToast( 76 | // context, 77 | // "${AppLocalizations.of(context)!.failed} : $e", 78 | // ); 79 | // } 80 | // } 81 | // } 82 | // } 83 | } 84 | 85 | /// 打开web页面 86 | Future openUrl(String url) async { 87 | if (await canLaunch(url)) { 88 | await launch( 89 | url, 90 | forceSafariVC: false, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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/image_cache_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'dart:ui' as ui show Codec; 5 | 6 | import 'ffi.dart'; 7 | 8 | class ImageCacheProvider extends ImageProvider { 9 | final String url; 10 | final String useful; 11 | final double scale; 12 | final int? extendsFieldIntFirst; 13 | final int? extendsFieldIntSecond; 14 | final int? extendsFieldIntThird; 15 | 16 | ImageCacheProvider({ 17 | required this.url, 18 | required this.useful, 19 | this.extendsFieldIntFirst, 20 | this.extendsFieldIntSecond, 21 | this.extendsFieldIntThird, 22 | this.scale = 1.0, 23 | }); 24 | 25 | @override 26 | ImageStreamCompleter load(ImageCacheProvider key, DecoderCallback decode) { 27 | return MultiFrameImageStreamCompleter( 28 | codec: _loadAsync(key), 29 | scale: key.scale, 30 | ); 31 | } 32 | 33 | @override 34 | Future obtainKey(ImageConfiguration configuration) { 35 | return SynchronousFuture(this); 36 | } 37 | 38 | Future _loadAsync(ImageCacheProvider key) async { 39 | assert(key == this); 40 | return PaintingBinding.instance!.instantiateImageCodec( 41 | await _loadImageFile((await native.loadCacheImage( 42 | url: url, 43 | useful: useful, 44 | extendsFieldIntFirst: extendsFieldIntFirst, 45 | extendsFieldIntSecond: extendsFieldIntSecond, 46 | extendsFieldIntThird: extendsFieldIntThird, 47 | )) 48 | .absPath), 49 | ); 50 | } 51 | 52 | @override 53 | bool operator ==(dynamic other) { 54 | if (other.runtimeType != runtimeType) return false; 55 | final ImageCacheProvider typedOther = other; 56 | return url == typedOther.url && scale == typedOther.scale; 57 | } 58 | 59 | @override 60 | int get hashCode => hashValues(url, scale); 61 | 62 | @override 63 | String toString() => '$runtimeType(' 64 | 'path: ${describeIdentity(url)},' 65 | ' scale: $scale' 66 | ')'; 67 | } 68 | 69 | Future _loadImageFile(String path) { 70 | return File(path).readAsBytes(); 71 | } 72 | 73 | class DownloadImageProvider extends ImageProvider { 74 | final String dlKey; 75 | final double scale; 76 | 77 | DownloadImageProvider({ 78 | required this.dlKey, 79 | this.scale = 1.0, 80 | }); 81 | 82 | @override 83 | ImageStreamCompleter load(DownloadImageProvider key, DecoderCallback decode) { 84 | return MultiFrameImageStreamCompleter( 85 | codec: _loadAsync(key), 86 | scale: key.scale, 87 | ); 88 | } 89 | 90 | @override 91 | Future obtainKey(ImageConfiguration configuration) { 92 | return SynchronousFuture(this); 93 | } 94 | 95 | Future _loadAsync(DownloadImageProvider key) async { 96 | assert(key == this); 97 | return PaintingBinding.instance!.instantiateImageCodec( 98 | await _loadImageFile((await native.loadDlImage( 99 | dlKey: dlKey, 100 | ))), 101 | ); 102 | } 103 | 104 | @override 105 | bool operator ==(dynamic other) { 106 | if (other.runtimeType != runtimeType) return false; 107 | final DownloadImageProvider typedOther = other; 108 | return dlKey == typedOther.dlKey && scale == typedOther.scale; 109 | } 110 | 111 | @override 112 | int get hashCode => hashValues(dlKey, scale); 113 | 114 | @override 115 | String toString() => '$runtimeType(' 116 | ' dlKey: ${describeIdentity(dlKey)},' 117 | ' scale: $scale' 118 | ')'; 119 | } 120 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "all": "All", 3 | "loading": "Loading", 4 | "exceptionTapToRetry": "Exception, Tap to retry", 5 | "chooseTag": "Choose tag", 6 | "networkConnectionFailed": "Network connection failed", 7 | "permissionDenied": "Permission denied", 8 | "pleaseCheckTheTimeOfTheDevice": "Please check the time of the device", 9 | "itIsBroken": "It's broken", 10 | "webSource": "Source", 11 | "switchToCom": "Switch to .com", 12 | "switchToNet": "Switch to .net", 13 | "switchToRe": "Switch to .re", 14 | "switchToXxx": "Switch to .xxx", 15 | "clean": "Clean", 16 | "cleaning": "Cleaning", 17 | "cleanFailed": "Clean failed", 18 | "cleanSuccess": "Clean success", 19 | "autoClean": "Auto clean", 20 | "checkingNewVersion": "Checking new version", 21 | "success": "Success", 22 | "failed": "Failed", 23 | "aYear": "Year", 24 | "aMonth": "Month", 25 | "aWeek": "Week", 26 | "aDay": "Day", 27 | "chooseAutoCleanPeriod": "Choose auto-clean period", 28 | "choosePagerColumnNumber": "Choose pager column number", 29 | "choosePagerMode": "Choose pager mode", 30 | "stream": "Stream", 31 | "pager": "Pager", 32 | "pleaseChoose": "Please choose", 33 | "saveImage": "Save image", 34 | "saveThisPreviewImage": "Save this preview image", 35 | "downloadOriginImage": "Download origin image", 36 | "ok": "Ok", 37 | "cancel": "Cancel", 38 | "proxy": "Proxy", 39 | "inputProxy": "Input proxy", 40 | "downloadAppendSuccess": "Append to download list success", 41 | "downloadAppendFailed": "Append to download list failed, because exists", 42 | "downloads": "Downloads", 43 | "downloading": "Downloading", 44 | "resetFailedDownloadsAndReload": "Reset failed downloads\nAnd list reloaded", 45 | "delete": "Delete", 46 | "pressBackKeyAgainToExit": "Press back key again to exit" 47 | } 48 | -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "all": "所有", 3 | "loading": "加载中", 4 | "exceptionTapToRetry": "出错, 点击重试", 5 | "chooseTag": "选择标签", 6 | "networkConnectionFailed": "网络连接失败", 7 | "permissionDenied": "无权限", 8 | "pleaseCheckTheTimeOfTheDevice": "请检查设备的时间", 9 | "itIsBroken": "被玩坏了", 10 | "webSource": "源", 11 | "switchToCom": "切换到.COM", 12 | "switchToNet": "切换到.NET", 13 | "switchToRe": "切换到.RE", 14 | "switchToXxx": "切换到.XXX", 15 | "clean": "清理", 16 | "cleaning": "清理中", 17 | "cleanFailed": "清理失败", 18 | "cleanSuccess": "清理成功", 19 | "autoClean": "自动清理", 20 | "checkingNewVersion": "正在检查更新", 21 | "success": "成功", 22 | "failed": "失败", 23 | "aYear": "一年", 24 | "aMonth": "一月", 25 | "aWeek": "一周", 26 | "aDay": "一天", 27 | "chooseAutoCleanPeriod":"选择自动清理时间", 28 | "choosePagerColumnNumber": "选择页面列数", 29 | "choosePagerMode": "选择分页模式", 30 | "stream": "流式", 31 | "pager": "分页", 32 | "pleaseChoose": "请选择", 33 | "saveImage": "保存图片", 34 | "saveThisPreviewImage": "保存这张预览图", 35 | "downloadOriginImage": "下载原图", 36 | "ok": "确定", 37 | "cancel": "取消", 38 | "proxy": "代理", 39 | "inputProxy": "请输入代理", 40 | "downloadAppendSuccess": "成功加入下载队列", 41 | "downloadAppendFailed": "未能加入下载队列, 请检查是否已经存在", 42 | "downloads": "下载", 43 | "downloading": "下载中", 44 | "resetFailedDownloadsAndReload": "重新下载失败的内容\n页面已刷新", 45 | "delete": "删除", 46 | "pressBackKeyAgainToExit": "再次按下返回键退出" 47 | } 48 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/configs/themes.dart'; 2 | import 'package:coco/screens/components/mouse_and_touch_scroll_behavior.dart'; 3 | import 'package:coco/screens/init_screen.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | import 'package:flutter_localizations/flutter_localizations.dart'; 7 | 8 | void main() { 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | const MyApp({Key? key}) : super(key: key); 14 | 15 | // This widget is the root of your application. 16 | @override 17 | Widget build(BuildContext context) { 18 | return MaterialApp( 19 | debugShowCheckedModeBanner: false, 20 | scrollBehavior: mouseAndTouchScrollBehavior, 21 | localizationsDelegates: const [ 22 | AppLocalizations.delegate, 23 | GlobalMaterialLocalizations.delegate, 24 | GlobalWidgetsLocalizations.delegate, 25 | GlobalCupertinoLocalizations.delegate, 26 | ], 27 | supportedLocales: AppLocalizations.supportedLocales, 28 | darkTheme: darkTheme, 29 | home: const InitScreen(), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/screens/about_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../configs/versions.dart'; 4 | import '../cross.dart'; 5 | import 'components/badge.dart'; 6 | import 'components/right_click_pop.dart'; 7 | 8 | class AboutScreen extends StatefulWidget { 9 | const AboutScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | State createState() { 13 | return _AboutState(); 14 | } 15 | } 16 | 17 | class _AboutState extends State { 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return rightClickPop(child: buildScreen(context), context: context); 22 | } 23 | 24 | Widget buildScreen(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: const Text("关于"), 28 | ), 29 | body: ListView( 30 | children: [ 31 | const Divider(), 32 | _buildLogo(), 33 | const Divider(), 34 | _buildCurrentVersion(), 35 | const Divider(), 36 | _buildNewestVersion(), 37 | const Divider(), 38 | _buildGotoGithub(), 39 | const Divider(), 40 | _buildVersionText(), 41 | const Divider(), 42 | ], 43 | ), 44 | ); 45 | } 46 | 47 | Widget _buildLogo() { 48 | return LayoutBuilder( 49 | builder: (BuildContext context, BoxConstraints constraints) { 50 | double? width, height; 51 | if (constraints.maxWidth < constraints.maxHeight) { 52 | width = constraints.maxWidth / 2; 53 | } else { 54 | height = constraints.maxHeight / 2; 55 | } 56 | return Container( 57 | padding: const EdgeInsets.all(10), 58 | child: Center( 59 | child: SizedBox( 60 | width: width, 61 | height: height, 62 | child: Image.asset( 63 | "lib/assets/startup.png", 64 | fit: BoxFit.contain, 65 | ), 66 | ), 67 | ), 68 | ); 69 | }, 70 | ); 71 | } 72 | 73 | Widget _buildCurrentVersion() { 74 | return Container( 75 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), 76 | child: Text("当前版本 : ${currentVersion()}"), 77 | ); 78 | } 79 | 80 | Widget _buildNewestVersion() { 81 | return Container( 82 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), 83 | child: Text.rich(TextSpan( 84 | children: [ 85 | const TextSpan(text: "最新版本 : "), 86 | _buildNewestVersionSpan(), 87 | _buildCheckButton(), 88 | ], 89 | )), 90 | ); 91 | } 92 | 93 | InlineSpan _buildNewestVersionSpan() { 94 | return WidgetSpan( 95 | child: Container( 96 | padding: const EdgeInsets.only(right: 20), 97 | child: VersionBadged( 98 | child: Text( 99 | "${latestVersion ?? "没有检测到新版本"} ", 100 | ), 101 | ), 102 | ), 103 | ); 104 | } 105 | 106 | InlineSpan _buildCheckButton() { 107 | return WidgetSpan( 108 | child: GestureDetector( 109 | child: const Text( 110 | "检查更新", 111 | style: TextStyle(height: 1.3, color: Colors.blue), 112 | strutStyle: StrutStyle(height: 1.3), 113 | ), 114 | onTap: () { 115 | manualCheckNewVersion(context); 116 | }, 117 | ), 118 | ); 119 | } 120 | 121 | Widget _buildGotoGithub() { 122 | return Container( 123 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), 124 | child: GestureDetector( 125 | child: const Text( 126 | "去下载地址", 127 | style: TextStyle(color: Colors.blue), 128 | ), 129 | onTap: () { 130 | openUrl("https://github.com/niuhuan/coco/releases/"); 131 | }, 132 | ), 133 | ); 134 | } 135 | 136 | Widget _buildVersionText() { 137 | var info = latestVersionInfo(); 138 | if (info != null) { 139 | info = "更新内容\n\n$info"; 140 | } 141 | return Container( 142 | padding: const EdgeInsets.all(20), 143 | child: SelectableText(info ?? ""), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/screens/components/badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../configs/versions.dart'; 3 | 4 | // 提示信息, 组件右上角的小红点 5 | class Badged extends StatelessWidget { 6 | final String? badge; 7 | final Widget child; 8 | 9 | const Badged({Key? key, required this.child, this.badge}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | if (badge == null) { 14 | return child; 15 | } 16 | return Stack( 17 | children: [ 18 | child, 19 | Positioned( 20 | right: 0, 21 | child: Container( 22 | padding: const EdgeInsets.all(1), 23 | decoration: BoxDecoration( 24 | color: Colors.red, 25 | borderRadius: BorderRadius.circular(6), 26 | ), 27 | constraints: const BoxConstraints( 28 | minWidth: 12, 29 | minHeight: 12, 30 | ), 31 | child: Text( 32 | badge!, 33 | style: const TextStyle( 34 | color: Colors.white, 35 | fontSize: 8, 36 | ), 37 | textAlign: TextAlign.center, 38 | ), 39 | ), 40 | ), 41 | ], 42 | ); 43 | } 44 | } 45 | 46 | class VersionBadged extends StatefulWidget { 47 | final Widget child; 48 | 49 | const VersionBadged({required this.child, Key? key}) : super(key: key); 50 | 51 | @override 52 | State createState() => _VersionBadgedState(); 53 | } 54 | 55 | class _VersionBadgedState extends State { 56 | @override 57 | void initState() { 58 | versionEvent.subscribe(_onVersion); 59 | super.initState(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | versionEvent.unsubscribe(_onVersion); 65 | super.dispose(); 66 | } 67 | 68 | void _onVersion(dynamic a) { 69 | setState(() {}); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Badged( 75 | child: widget.child, 76 | badge: latestVersion == null ? null : "1", 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/screens/components/browser_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/configs/proxy.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | import '../../commons.dart'; 7 | import '../../configs/auto_clean.dart'; 8 | import '../../configs/host.dart'; 9 | import '../../configs/pager_column_number.dart'; 10 | import '../../configs/pager_controller_mode.dart'; 11 | import '../../ffi.dart'; 12 | 13 | class BrowserBottomSheetAction extends StatelessWidget { 14 | const BrowserBottomSheetAction({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return IconButton( 19 | onPressed: () { 20 | _displayBrowserBottomSheet(context); 21 | }, 22 | icon: const Icon(Icons.menu), 23 | ); 24 | } 25 | } 26 | 27 | Future _displayBrowserBottomSheet(BuildContext context) async { 28 | await showMaterialModalBottomSheet( 29 | context: context, 30 | backgroundColor: const Color(0xAA000000), 31 | builder: (context) { 32 | return SizedBox( 33 | height: MediaQuery.of(context).size.height * (.45), 34 | child: _BrowserBottomSheet(), 35 | ); 36 | }, 37 | ); 38 | } 39 | 40 | class _BrowserBottomSheet extends StatefulWidget { 41 | @override 42 | State createState() => _BrowserBottomSheetState(); 43 | } 44 | 45 | class _BrowserBottomSheetState extends State<_BrowserBottomSheet> { 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | var hostName = ""; 50 | if (host == hosts[0]) { 51 | hostName = ".NET"; 52 | } 53 | else if (host == hosts[1]) { 54 | hostName = ".COM"; 55 | } 56 | else if (host == hosts[2]) { 57 | hostName = ".RE"; 58 | } 59 | else if (host == hosts[3]) { 60 | hostName = ".XXX"; 61 | } 62 | return ListView( 63 | children: [ 64 | Row( 65 | children: [ 66 | Expanded(child: Container()), 67 | _bottomIcon( 68 | icon: Icons.repeat_one, 69 | title: hostName, 70 | onPressed: () async { 71 | await chooseHost(context); 72 | setState(() {}); 73 | }, 74 | ), 75 | Expanded(child: Container()), 76 | _bottomIcon( 77 | icon: Icons.view_day_outlined, 78 | title: currentPagerControllerModeName(context), 79 | onPressed: () async { 80 | await choosePagerControllerMode(context); 81 | setState(() {}); 82 | }, 83 | ), 84 | Expanded(child: Container()), 85 | _bottomIcon( 86 | icon: Icons.view_column_sharp, 87 | title: "$pagerColumnNumber", 88 | onPressed: () async { 89 | await choosePagerColumnCount(context); 90 | setState(() {}); 91 | }, 92 | ), 93 | Expanded(child: Container()), 94 | ], 95 | ), 96 | Row( 97 | children: [ 98 | Expanded(child: Container()), 99 | _bottomIcon( 100 | icon: Icons.network_ping, 101 | title: "proxy", 102 | onPressed: () async { 103 | await inputProxy(context); 104 | setState(() {}); 105 | }, 106 | ), 107 | Expanded(child: Container()), 108 | _bottomIcon( 109 | icon: Icons.cleaning_services_rounded, 110 | title: AppLocalizations.of(context)!.clean, 111 | onPressed: () async { 112 | defaultToast(context, AppLocalizations.of(context)!.cleaning); 113 | try { 114 | await native.cleanAllCache(); 115 | defaultToast(context, AppLocalizations.of(context)!.cleanSuccess); 116 | } catch (e) { 117 | print("$e"); 118 | defaultToast(context, AppLocalizations.of(context)!.cleanFailed); 119 | } 120 | setState(() {}); 121 | }, 122 | ), 123 | Expanded(child: Container()), 124 | _bottomIcon( 125 | icon: Icons.auto_delete_outlined, 126 | title: autoCleanName(context), 127 | onPressed: () async { 128 | await chooseAutoClean(context); 129 | setState(() {}); 130 | }, 131 | ), 132 | Expanded(child: Container()), 133 | ], 134 | ), 135 | ], 136 | ); 137 | } 138 | 139 | Widget _bottomIcon({ 140 | required IconData icon, 141 | required String title, 142 | required void Function() onPressed, 143 | }) { 144 | return Expanded( 145 | child: Center( 146 | child: Column( 147 | children: [ 148 | IconButton( 149 | iconSize: 55, 150 | icon: Column( 151 | children: [ 152 | Container(height: 3), 153 | Icon( 154 | icon, 155 | size: 25, 156 | color: Colors.white, 157 | ), 158 | Container(height: 3), 159 | Text( 160 | title, 161 | style: const TextStyle(color: Colors.white, fontSize: 10), 162 | maxLines: 1, 163 | textAlign: TextAlign.center, 164 | ), 165 | Container(height: 3), 166 | ], 167 | ), 168 | onPressed: onPressed, 169 | ) 170 | ], 171 | ), 172 | ), 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/screens/components/content_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'content_error.dart'; 3 | import 'content_loading.dart'; 4 | 5 | class ContentBuilder extends StatelessWidget { 6 | final Future future; 7 | final Future Function() onRefresh; 8 | final AsyncWidgetBuilder successBuilder; 9 | final String? loadingLabel; 10 | 11 | const ContentBuilder({ 12 | Key? key, 13 | required this.future, 14 | required this.onRefresh, 15 | required this.successBuilder, 16 | this.loadingLabel, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return FutureBuilder( 22 | future: future, 23 | builder: (BuildContext context, AsyncSnapshot snapshot) { 24 | if (snapshot.hasError) { 25 | return ContentError( 26 | error: snapshot.error, 27 | stackTrace: snapshot.stackTrace, 28 | onRefresh: onRefresh, 29 | ); 30 | } 31 | if (snapshot.connectionState != ConnectionState.done) { 32 | return ContentLoading(label: loadingLabel); 33 | } 34 | return successBuilder(context, snapshot); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/screens/components/content_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'error_types.dart'; 5 | 6 | class ContentError extends StatelessWidget { 7 | final Object? error; 8 | final StackTrace? stackTrace; 9 | final Future Function() onRefresh; 10 | 11 | const ContentError({ 12 | Key? key, 13 | required this.error, 14 | required this.stackTrace, 15 | required this.onRefresh, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | var type = errorType("$error"); 21 | late String message; 22 | late IconData iconData; 23 | switch (type) { 24 | case ERROR_TYPE_NETWORK: 25 | iconData = Icons.wifi_off_rounded; 26 | message = AppLocalizations.of(context)!.networkConnectionFailed; 27 | break; 28 | case ERROR_TYPE_PERMISSION: 29 | iconData = Icons.highlight_off; 30 | message = AppLocalizations.of(context)!.permissionDenied; 31 | break; 32 | case ERROR_TYPE_TIME: 33 | iconData = Icons.timer_off; 34 | message = AppLocalizations.of(context)!.pleaseCheckTheTimeOfTheDevice; 35 | break; 36 | default: 37 | iconData = Icons.highlight_off; 38 | message = AppLocalizations.of(context)!.itIsBroken; 39 | break; 40 | } 41 | return LayoutBuilder( 42 | builder: (BuildContext context, BoxConstraints constraints) { 43 | print("$error"); 44 | print("$stackTrace"); 45 | var width = constraints.maxWidth; 46 | var height = constraints.maxHeight; 47 | var min = width < height ? width : height; 48 | var iconSize = min / 2.3; 49 | var textSize = min / 16; 50 | var tipSize = min / 20; 51 | var infoSize = min / 30; 52 | return GestureDetector( 53 | onTap: onRefresh, 54 | child: ListView( 55 | children: [ 56 | SizedBox( 57 | height: height, 58 | child: Column( 59 | children: [ 60 | Expanded(child: Container()), 61 | Icon( 62 | iconData, 63 | size: iconSize, 64 | color: Colors.grey.shade600, 65 | ), 66 | Container(height: min / 10), 67 | Container( 68 | padding: const EdgeInsets.only( 69 | left: 30, 70 | right: 30, 71 | ), 72 | child: Text( 73 | message, 74 | style: TextStyle(fontSize: textSize), 75 | textAlign: TextAlign.center, 76 | ), 77 | ), 78 | Text( 79 | AppLocalizations.of(context)!.exceptionTapToRetry, 80 | style: TextStyle(fontSize: tipSize), 81 | ), 82 | Container(height: min / 15), 83 | Text('$error', style: TextStyle(fontSize: infoSize)), 84 | Expanded(child: Container()), 85 | ], 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | }, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/screens/components/content_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ContentLoading extends StatelessWidget { 5 | final String? label; 6 | 7 | const ContentLoading({Key? key, this.label}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | String label = this.label ?? AppLocalizations.of(context)!.loading; 12 | return LayoutBuilder( 13 | builder: (BuildContext context, BoxConstraints constraints) { 14 | var width = constraints.maxWidth; 15 | var height = constraints.maxHeight; 16 | var min = width < height ? width : height; 17 | var theme = Theme.of(context); 18 | return Center( 19 | child: Column( 20 | children: [ 21 | Expanded(child: Container()), 22 | SizedBox( 23 | width: min / 2, 24 | height: min / 2, 25 | child: CircularProgressIndicator( 26 | color: theme.colorScheme.secondary, 27 | backgroundColor: Colors.grey[100], 28 | ), 29 | ), 30 | Container(height: min / 10), 31 | Text(label, style: TextStyle(fontSize: min / 15)), 32 | Expanded(child: Container()), 33 | ], 34 | ), 35 | ); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/screens/components/empty_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget { 4 | final Color color; 5 | 6 | const EmptyAppBar({Key? key, this.color = Colors.black}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | color: color, 12 | ); 13 | } 14 | 15 | @override 16 | Size get preferredSize => const Size(0.0, 0.0); 17 | } 18 | -------------------------------------------------------------------------------- /lib/screens/components/error_types.dart: -------------------------------------------------------------------------------- 1 | const ERROR_TYPE_NETWORK = "NETWORK_ERROR"; 2 | const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR"; 3 | const ERROR_TYPE_TIME = "TIME_ERROR"; 4 | 5 | // 错误的类型, 方便照展示和谐的提示 6 | String errorType(String error) { 7 | // EXCEPTION 8 | // Get "https://****": net/http: TLS handshake timeout 9 | // Get "https://****": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused 10 | // Get "https://****": context deadline exceeded (Client.Timeout exceeded while awaiting headers) 11 | if (error.contains("timeout") || 12 | error.contains("connection refused") || 13 | error.contains("deadline") || 14 | error.contains("connection abort")) { 15 | return ERROR_TYPE_NETWORK; 16 | } 17 | if (error.contains("permission denied")) { 18 | return ERROR_TYPE_PERMISSION; 19 | } 20 | if (error.contains("time is not synchronize")) { 21 | return ERROR_TYPE_TIME; 22 | } 23 | return ""; 24 | } 25 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /lib/screens/components/right_click_pop.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Widget rightClickPop({ 4 | required Widget child, 5 | required BuildContext context, 6 | bool canPop = true, 7 | }) => 8 | GestureDetector( 9 | onSecondaryTap: () { 10 | if (canPop) { 11 | Navigator.of(context).pop(); 12 | } 13 | }, 14 | child: child, 15 | ); 16 | -------------------------------------------------------------------------------- /lib/screens/components/shadow_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:decorated_icon/decorated_icon.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ShadowIconButton extends StatelessWidget { 5 | final Function() onPressed; 6 | final IconData icon; 7 | 8 | const ShadowIconButton( 9 | {Key? key, required this.onPressed, required this.icon}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return IconButton( 15 | color: Colors.white, 16 | icon: DecoratedIcon( 17 | icon, 18 | size: 24, 19 | shadows: const [ 20 | BoxShadow( 21 | color: Colors.black, 22 | offset: Offset(1.0, 1.0), 23 | blurRadius: 5.0, 24 | ), 25 | ], 26 | ), 27 | onPressed: onPressed, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/screens/init_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:coco/configs/auto_clean.dart'; 4 | import 'package:coco/configs/host.dart'; 5 | import 'package:coco/configs/pager_column_number.dart'; 6 | import 'package:coco/configs/versions.dart'; 7 | import 'package:coco/ffi.dart'; 8 | import 'package:coco/screens/posts_screen.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:permission_handler/permission_handler.dart'; 11 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 12 | import '../commons.dart'; 13 | import '../configs/pager_controller_mode.dart'; 14 | import '../configs/platform.dart'; 15 | import '../configs/proxy.dart'; 16 | import '../cross.dart'; 17 | 18 | class InitScreen extends StatefulWidget { 19 | const InitScreen({Key? key}) : super(key: key); 20 | 21 | @override 22 | State createState() => _InitScreenState(); 23 | } 24 | 25 | class _InitScreenState extends State { 26 | Future _init() async { 27 | await initPlatform(); 28 | if (Platform.isAndroid) { 29 | late bool g; 30 | if (androidVersion < 30) { 31 | g = await Permission.storage.request().isGranted; 32 | } else { 33 | g = await Permission.manageExternalStorage.request().isGranted; 34 | } 35 | if (!g) { 36 | defaultToast( 37 | context, 38 | AppLocalizations.of(context)!.permissionDenied, 39 | ); 40 | exit(0); 41 | } 42 | } 43 | await native.init( 44 | root: await cross.root(), 45 | downloadsTo: await cross.downloads(), 46 | ); 47 | await initHost(); 48 | await initPagerColumnCount(); 49 | await initPagerControllerMode(); 50 | await initVersion(); 51 | await initAutoClean(); 52 | await initProxy(); 53 | autoCheckNewVersion(); 54 | native.resetFailedDownloads(); // async 55 | Navigator.of(context).pushReplacement(MaterialPageRoute( 56 | builder: (BuildContext context) { 57 | return const PostsScreen(); 58 | }, 59 | )); 60 | } 61 | 62 | @override 63 | void initState() { 64 | _init(); 65 | super.initState(); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return Scaffold( 71 | backgroundColor: const Color(0xffbbb6a4), 72 | body: ConstrainedBox( 73 | constraints: const BoxConstraints.expand(), 74 | child: LayoutBuilder( 75 | builder: (BuildContext context, BoxConstraints constraints) { 76 | var min = constraints.maxWidth > constraints.maxHeight 77 | ? constraints.maxHeight 78 | : constraints.maxWidth; 79 | var padding = min / 6; 80 | return Container( 81 | padding: EdgeInsets.all(padding), 82 | child: Image.asset( 83 | "lib/assets/startup.png", 84 | fit: BoxFit.contain, 85 | ), 86 | ); 87 | }, 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/screens/posts_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/configs/host.dart'; 2 | import 'package:coco/screens/about_screen.dart'; 3 | import 'package:coco/screens/components/badge.dart'; 4 | import 'package:coco/screens/components/post_pager.dart'; 5 | import 'package:coco/screens/tag_choose_screen.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 8 | import 'package:flutter_styled_toast/flutter_styled_toast.dart'; 9 | 10 | import 'components/browser_bottom_sheet.dart'; 11 | import 'downloading_screen.dart'; 12 | 13 | class PostsScreen extends StatefulWidget { 14 | const PostsScreen({ 15 | Key? key, 16 | }) : super(key: key); 17 | 18 | @override 19 | State createState() => _PostsScreenState(); 20 | } 21 | 22 | class _PostsScreenState extends State { 23 | String _tags = ""; 24 | 25 | @override 26 | void initState() { 27 | hostEvent.subscribe(_setState); 28 | super.initState(); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | hostEvent.unsubscribe(_setState); 34 | super.dispose(); 35 | } 36 | 37 | _setState(_) { 38 | setState(() {}); 39 | } 40 | 41 | var _noticeTime = 0; 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return WillPopScope( 46 | child: buildScreen(context), 47 | onWillPop: () async { 48 | final now = DateTime.now().millisecondsSinceEpoch; 49 | if (_noticeTime + 3000 > now) { 50 | return true; 51 | } else { 52 | _noticeTime = now; 53 | showToast( 54 | AppLocalizations.of(context)!.pressBackKeyAgainToExit, 55 | context: context, 56 | position: StyledToastPosition.center, 57 | animation: StyledToastAnimation.scale, 58 | reverseAnimation: StyledToastAnimation.fade, 59 | duration: const Duration(seconds: 3), 60 | animDuration: const Duration(milliseconds: 300), 61 | curve: Curves.elasticOut, 62 | reverseCurve: Curves.linear, 63 | ); 64 | return false; 65 | } 66 | return true; 67 | }, 68 | ); 69 | } 70 | 71 | Widget buildScreen(BuildContext context) { 72 | final tags = _tags == "" ? AppLocalizations.of(context)!.all : _tags; 73 | return Scaffold( 74 | appBar: AppBar( 75 | title: Text(tags), 76 | actions: [ 77 | IconButton( 78 | onPressed: () { 79 | Navigator.of(context).push( 80 | MaterialPageRoute(builder: (BuildContext context) { 81 | return const AboutScreen(); 82 | }), 83 | ); 84 | }, 85 | icon: const VersionBadged( 86 | child: Icon(Icons.info_outline), 87 | )), 88 | const BrowserBottomSheetAction(), 89 | _downloadsAction(), 90 | _buildTagAction(), 91 | ], 92 | ), 93 | body: PostPager( 94 | host: host, 95 | tags: _tags, 96 | listener: PostPagerListener(onTag: _onTag), 97 | ), 98 | ); 99 | } 100 | 101 | Widget _buildTagAction() { 102 | return IconButton( 103 | onPressed: () async { 104 | String? choose = await Navigator.of(context).push(MaterialPageRoute( 105 | builder: (BuildContext context) { 106 | return TagChooseScreen(host: host, initTags: _tags); 107 | }, 108 | )); 109 | if (choose != null) { 110 | _onTag(choose); 111 | } 112 | }, 113 | icon: const Icon(Icons.style), 114 | ); 115 | } 116 | 117 | Widget _downloadsAction() { 118 | return IconButton( 119 | onPressed: () async { 120 | await Navigator.of(context).push(MaterialPageRoute( 121 | builder: (BuildContext context) { 122 | return const DownloadingScreen(); 123 | }, 124 | )); 125 | }, 126 | icon: const Icon(Icons.download), 127 | ); 128 | } 129 | 130 | void _onTag(String choose) { 131 | setState(() { 132 | _tags = choose; 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/screens/tag_choose_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:coco/ffi.dart'; 2 | import 'package:coco/screens/components/content_builder.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | import 'components/right_click_pop.dart'; 7 | 8 | class TagChooseScreen extends StatefulWidget { 9 | final String host; 10 | final String initTags; 11 | 12 | const TagChooseScreen({ 13 | required this.host, 14 | required this.initTags, 15 | Key? key, 16 | }) : super(key: key); 17 | 18 | @override 19 | State createState() => _TagChooseScreenState(); 20 | } 21 | 22 | class _TagChooseScreenState extends State { 23 | late final TextEditingController _editingController = TextEditingController(); 24 | 25 | late Future _tagDataFuture; 26 | late Key _tagDataFutureKey; 27 | 28 | void _reset() { 29 | _tagDataFuture = native.tagSummary(host: widget.host); 30 | _tagDataFutureKey = UniqueKey(); 31 | } 32 | 33 | @override 34 | void initState() { 35 | _editingController.text = widget.initTags; 36 | _reset(); 37 | super.initState(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _editingController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return rightClickPop(child: buildScreen(context), context: context); 49 | } 50 | 51 | Widget buildScreen(BuildContext context) { 52 | return Scaffold( 53 | appBar: AppBar( 54 | title: Text(AppLocalizations.of(context)!.chooseTag), 55 | actions: [ 56 | _buildNoTags(), 57 | ], 58 | ), 59 | body: _buildBody(), 60 | ); 61 | } 62 | 63 | Widget _buildNoTags() { 64 | return IconButton( 65 | onPressed: () { 66 | Navigator.of(context).pop(""); 67 | }, 68 | icon: const Icon(Icons.clear), 69 | ); 70 | } 71 | 72 | Widget _buildBody() { 73 | return ContentBuilder( 74 | key: _tagDataFutureKey, 75 | future: _tagDataFuture, 76 | onRefresh: () async { 77 | setState(() { 78 | _reset(); 79 | }); 80 | }, 81 | successBuilder: ( 82 | BuildContext context, 83 | AsyncSnapshot snapshot, 84 | ) { 85 | final data = snapshot.requireData; 86 | final list = data.tags; 87 | return Column(children: [ 88 | Container( 89 | padding: const EdgeInsets.fromLTRB(10, 10, 10, 5), 90 | child: TextFormField( 91 | controller: _editingController, 92 | onChanged: (_) { 93 | setState(() {}); 94 | }, 95 | decoration: const InputDecoration( 96 | border: UnderlineInputBorder(), 97 | ), 98 | ), 99 | ), 100 | Expanded( 101 | child: Container( 102 | padding: const EdgeInsets.fromLTRB(10, 5, 10, 10), 103 | child: _buildList(list), 104 | ), 105 | ), 106 | ]); 107 | }, 108 | ); 109 | } 110 | 111 | Widget _buildList(List list) { 112 | var text = _editingController.text; 113 | List newList = []; 114 | for (var value in list) { 115 | if (text.isEmpty || value.tagNames[0].contains(text)) { 116 | newList.add(value); 117 | } 118 | } 119 | list = newList; 120 | return ListView.builder( 121 | itemCount: list.length, 122 | itemBuilder: (BuildContext context, int index) { 123 | final item = list[index]; 124 | return GestureDetector( 125 | onTap: () { 126 | Navigator.of(context).pop(item.tagNames[0]); 127 | }, 128 | child: Container( 129 | decoration: BoxDecoration( 130 | border: Border.all( 131 | color: Theme.of(context).dividerColor, 132 | width: 0.5, 133 | style: BorderStyle.solid, 134 | ), 135 | ), 136 | padding: const EdgeInsets.all(10), 137 | child: Text(item.tagNames[0]), 138 | ), 139 | ); 140 | }, 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /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 "coco") 5 | set(APPLICATION_ID "niuhuan.coco") 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=coco 6 | Exec=AppRun %u 7 | Icon=AppRun 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /linux/appimage/AppRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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, "coco"); 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, "coco"); 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 path_provider_foundation 9 | import share_plus_macos 10 | import url_launcher_macos 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 14 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 15 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | 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/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - share_plus_macos (0.0.1): 7 | - FlutterMacOS 8 | - url_launcher_macos (0.0.1): 9 | - FlutterMacOS 10 | 11 | DEPENDENCIES: 12 | - FlutterMacOS (from `Flutter/ephemeral`) 13 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) 14 | - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) 15 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 16 | 17 | EXTERNAL SOURCES: 18 | FlutterMacOS: 19 | :path: Flutter/ephemeral 20 | path_provider_foundation: 21 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos 22 | share_plus_macos: 23 | :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos 24 | url_launcher_macos: 25 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 26 | 27 | SPEC CHECKSUMS: 28 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 29 | path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 30 | share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 31 | url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 32 | 33 | PODFILE CHECKSUM: a96161e6a6fd60fced74db1741370bd9da192d82 34 | 35 | COCOAPODS: 1.11.3 36 | -------------------------------------------------------------------------------- /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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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 = coco 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = niuhuan.coco 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.22" 17 | flutter_rust_bridge = "=1.54.1" 18 | hex = "0.4" 19 | image = { version = "0", features = ["jpeg", "gif", "webp", "bmp", "png", "jpeg_rayon"] } 20 | itertools = "0.10.3" 21 | lazy_static = "1" 22 | md5 = "0.7" 23 | once_cell = "1" 24 | prost = "0.11" 25 | prost-types = "0.11" 26 | regex = "1.5.5" 27 | reqwest = { version = "0.11", features = ["tokio-rustls", "rustls", "rustls-tls", "socks"], default-features = false } 28 | rsa = "0.7" 29 | rust-crypto = "0" 30 | sea-orm = { version = "0.10.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"], default-features = false } 31 | serde = "1.0" 32 | serde_derive = "1.0" 33 | serde_json = "1.0" 34 | serde_path_to_error = "0.1.7" 35 | tokio = { version = "1", features = ["full"] } 36 | 37 | [target.'cfg(any(target_os = "windows", target_os = "linux" , target_os = "macos"))'.dependencies] 38 | directories = { version = "5.0" } 39 | -------------------------------------------------------------------------------- /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 | 6 11 | 12 | native-staticlib.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 8 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/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod dl_post; 2 | 3 | use crate::database::connect_db; 4 | use once_cell::sync::OnceCell; 5 | use sea_orm::DatabaseConnection; 6 | use tokio::sync::Mutex; 7 | 8 | pub(crate) static ACTIVE_DATABASE: OnceCell> = OnceCell::new(); 9 | 10 | pub(crate) async fn init() { 11 | let db = connect_db("active.db").await; 12 | ACTIVE_DATABASE.set(Mutex::new(db)).unwrap(); 13 | dl_post::init().await; 14 | } 15 | -------------------------------------------------------------------------------- /native/src/database/cache/image_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::database::cache::CACHE_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::sea_query::Expr; 5 | use sea_orm::EntityTrait; 6 | use sea_orm::IntoActiveModel; 7 | use sea_orm::QueryOrder; 8 | use sea_orm::QuerySelect; 9 | use std::ops::Deref; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 12 | #[sea_orm(table_name = "image_cache")] 13 | pub struct Model { 14 | #[sea_orm(primary_key, auto_increment = false)] 15 | pub url: String, 16 | pub useful: String, 17 | pub extends_field_int_first: Option, 18 | pub extends_field_int_second: Option, 19 | pub extends_field_int_third: Option, 20 | pub local_path: String, 21 | pub cache_time: i64, 22 | pub image_format: String, 23 | pub image_width: u32, 24 | pub image_height: u32, 25 | } 26 | 27 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 28 | pub enum Relation {} 29 | 30 | impl ActiveModelBehavior for ActiveModel {} 31 | 32 | pub(crate) async fn init() { 33 | let gdb = CACHE_DATABASE.get().unwrap().lock().await; 34 | let db = gdb.deref(); 35 | create_table_if_not_exists(db, Entity).await; 36 | if !index_exists(db, "image_cache", "image_cache_idx_cache_time").await { 37 | create_index( 38 | db, 39 | "image_cache", 40 | vec!["cache_time"], 41 | "image_cache_idx_cache_time", 42 | ) 43 | .await; 44 | } 45 | } 46 | 47 | pub(crate) async fn load_image_by_url(url: String) -> anyhow::Result> { 48 | Ok(Entity::find_by_id(url) 49 | .one(CACHE_DATABASE.get().unwrap().lock().await.deref()) 50 | .await?) 51 | } 52 | 53 | pub(crate) async fn insert(model: Model) -> anyhow::Result { 54 | Ok(model 55 | .into_active_model() 56 | .insert(CACHE_DATABASE.get().unwrap().lock().await.deref()) 57 | .await?) 58 | } 59 | 60 | pub(crate) async fn update_cache_time(url: String) -> anyhow::Result<()> { 61 | Entity::update_many() 62 | .col_expr( 63 | Column::CacheTime, 64 | Expr::value(chrono::Local::now().timestamp_millis()), 65 | ) 66 | .filter(Column::Url.eq(url)) 67 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 68 | .await?; 69 | Ok(()) 70 | } 71 | 72 | pub(crate) async fn take_100_cache(time: i64) -> anyhow::Result> { 73 | Ok(Entity::find() 74 | .filter(Column::CacheTime.lt(time)) 75 | .order_by_asc(Column::CacheTime) 76 | .limit(100) 77 | .all(CACHE_DATABASE.get().unwrap().lock().await.deref()) 78 | .await?) 79 | } 80 | 81 | pub(crate) async fn delete_by_url(url: String) -> anyhow::Result<()> { 82 | Entity::delete_many() 83 | .filter(Column::Url.eq(url)) 84 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 85 | .await?; 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /native/src/database/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::connect_db; 2 | use once_cell::sync::OnceCell; 3 | use sea_orm::ConnectionTrait; 4 | use sea_orm::DatabaseConnection; 5 | use sea_orm::ExecResult; 6 | use sea_orm::Statement; 7 | use tokio::sync::Mutex; 8 | 9 | pub(crate) mod image_cache; 10 | pub(crate) mod web_cache; 11 | 12 | pub(crate) static CACHE_DATABASE: OnceCell> = OnceCell::new(); 13 | 14 | pub(crate) async fn init() { 15 | let db = connect_db("cache.db").await; 16 | CACHE_DATABASE.set(Mutex::new(db)).unwrap(); 17 | // init tables 18 | image_cache::init().await; 19 | web_cache::init().await; 20 | } 21 | 22 | pub(crate) async fn vacuum() -> anyhow::Result<()> { 23 | let db = CACHE_DATABASE.get().unwrap().lock().await; 24 | let backend = db.get_database_backend(); 25 | let _: ExecResult = db 26 | .execute(Statement::from_string(backend, "VACUUM".to_owned())) 27 | .await?; 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /native/src/database/cache/web_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::database::cache::CACHE_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::sea_query::Expr; 5 | use sea_orm::EntityTrait; 6 | use sea_orm::IntoActiveModel; 7 | use std::future::Future; 8 | use std::ops::Deref; 9 | use std::pin::Pin; 10 | use std::time::Duration; 11 | 12 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 13 | #[sea_orm(table_name = "web_cache")] 14 | pub struct Model { 15 | #[sea_orm(primary_key, auto_increment = false)] 16 | pub cache_key: String, 17 | pub cache_content: String, 18 | pub cache_time: i64, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 22 | pub enum Relation {} 23 | 24 | impl ActiveModelBehavior for ActiveModel {} 25 | 26 | pub(crate) async fn init() { 27 | let gdb = CACHE_DATABASE.get().unwrap().lock().await; 28 | let db = gdb.deref(); 29 | create_table_if_not_exists(db, Entity).await; 30 | if !index_exists(db, "web_cache", "web_cache_idx_cache_time").await { 31 | create_index( 32 | db, 33 | "web_cache", 34 | vec!["cache_time"], 35 | "web_cache_idx_cache_time", 36 | ) 37 | .await; 38 | } 39 | } 40 | 41 | pub(crate) async fn cache_first serde::Deserialize<'de> + serde::Serialize>( 42 | key: String, 43 | expire: Duration, 44 | pin: Pin>>>, 45 | ) -> anyhow::Result { 46 | let time = chrono::Local::now().timestamp_millis(); 47 | let db = CACHE_DATABASE.get().unwrap().lock().await; 48 | let in_db = Entity::find_by_id(key.clone()).one(db.deref()).await?; 49 | if let Some(ref model) = in_db { 50 | if time < (model.cache_time + expire.as_millis() as i64) { 51 | return Ok(serde_json::from_str(&model.cache_content)?); 52 | } 53 | }; 54 | let t = pin.await?; 55 | let content = serde_json::to_string(&t)?; 56 | if let Some(_) = in_db { 57 | Entity::update_many() 58 | .filter(Column::CacheKey.eq(key.clone())) 59 | .col_expr(Column::CacheTime, Expr::value(time.clone())) 60 | .col_expr(Column::CacheContent, Expr::value(content.clone())) 61 | .exec(db.deref()) 62 | .await?; 63 | } else { 64 | Model { 65 | cache_key: key, 66 | cache_content: content, 67 | cache_time: time, 68 | } 69 | .into_active_model() 70 | .insert(db.deref()) 71 | .await?; 72 | } 73 | Ok(t) 74 | } 75 | 76 | pub(crate) async fn clean_web_cache_by_time(time: i64) -> anyhow::Result<()> { 77 | Entity::delete_many() 78 | .filter(Column::CacheTime.lt(time)) 79 | .exec(CACHE_DATABASE.get().unwrap().lock().await.deref()) 80 | .await?; 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /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 | 116 | #[allow(dead_code)] 117 | pub(crate) async fn colum_exists( 118 | db: &DatabaseConnection, 119 | table_name: &str, 120 | column_name: &str, 121 | ) -> bool { 122 | let stmt = Statement::from_string( 123 | db.get_database_backend(), 124 | format!("PRAGMA table_info({});", table_name), 125 | ); 126 | let rsp = db.query_all(stmt).await.unwrap(); 127 | for x in rsp { 128 | let name: String = x.try_get("", "name").unwrap(); 129 | if column_name == name.as_str() { 130 | return true; 131 | }; 132 | } 133 | return false; 134 | } 135 | 136 | #[allow(dead_code)] 137 | pub(crate) async fn append_column( 138 | db: &DatabaseConnection, 139 | table_name: &str, 140 | column_name: &str, 141 | data_type: &str, 142 | ) { 143 | let stmt = Statement::from_string( 144 | db.get_database_backend(), 145 | format!( 146 | "ALTER TABLE {} ADD COLUMN {} {} NOT NULL;", 147 | table_name, column_name, data_type, 148 | ), 149 | ); 150 | db.execute(stmt).await.unwrap(); 151 | } 152 | -------------------------------------------------------------------------------- /native/src/database/properties/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::database::connect_db; 2 | use once_cell::sync::OnceCell; 3 | use sea_orm::DatabaseConnection; 4 | use tokio::sync::Mutex; 5 | 6 | pub(crate) mod property; 7 | 8 | pub(crate) static PROPERTIES_DATABASE: OnceCell> = OnceCell::new(); 9 | 10 | pub(crate) async fn init() { 11 | let db = connect_db("properties.db").await; 12 | PROPERTIES_DATABASE.set(Mutex::new(db)).unwrap(); 13 | // init tables 14 | property::init().await; 15 | } 16 | -------------------------------------------------------------------------------- /native/src/database/properties/property.rs: -------------------------------------------------------------------------------- 1 | use crate::database::properties::PROPERTIES_DATABASE; 2 | use crate::database::{create_index, create_table_if_not_exists, index_exists}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::IntoActiveModel; 5 | use sea_orm::{EntityTrait, Set}; 6 | use std::ops::Deref; 7 | 8 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] 9 | #[sea_orm(table_name = "property")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub k: String, 13 | pub v: String, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 17 | pub enum Relation {} 18 | 19 | impl ActiveModelBehavior for ActiveModel {} 20 | 21 | pub(crate) async fn init() { 22 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 23 | create_table_if_not_exists(db.deref(), Entity).await; 24 | if !index_exists(db.deref(), "property", "property_idx_k").await { 25 | create_index(db.deref(), "property", vec!["k"], "property_idx_k").await; 26 | } 27 | } 28 | 29 | pub async fn save_property(k: String, v: String) -> anyhow::Result<()> { 30 | let db = PROPERTIES_DATABASE.get().unwrap().lock().await; 31 | if let Some(in_db) = Entity::find_by_id(k.clone()).one(db.deref()).await? { 32 | let mut in_db = in_db.into_active_model(); 33 | in_db.v = Set(v); 34 | in_db.update(db.deref()).await?; 35 | } else { 36 | Model { k, v } 37 | .into_active_model() 38 | .insert(db.deref()) 39 | .await?; 40 | } 41 | Ok(()) 42 | } 43 | 44 | pub async fn load_property(k: String) -> anyhow::Result { 45 | let in_db = Entity::find_by_id(k) 46 | .one(PROPERTIES_DATABASE.get().unwrap().lock().await.deref()) 47 | .await?; 48 | Ok(if let Some(in_db) = in_db { 49 | in_db.v 50 | } else { 51 | "".to_owned() 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /native/src/download.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::time; 3 | 4 | use crate::api::down_image; 5 | use crate::database::active::dl_post; 6 | use crate::database::active::dl_post::{ 7 | delete_download_info, first_need_delete_post, first_not_result_post, 8 | }; 9 | use crate::{get_downloads_dir, join_paths}; 10 | 11 | pub(crate) async fn download_demon() { 12 | loop { 13 | loop { 14 | if let Some(info) = first_need_delete_post().await.unwrap() { 15 | let local_path = info.dl_key.clone(); 16 | let abs_path = join_paths(vec![get_downloads_dir().as_str(), &local_path]); 17 | if Path::new(&abs_path).exists() { 18 | let _ = tokio::fs::remove_file(&abs_path).await; 19 | } 20 | let _ = delete_download_info(info.dl_key).await; 21 | // todo notify 22 | } else { 23 | break; 24 | } 25 | } 26 | if let Some(info) = first_not_result_post().await.unwrap() { 27 | down_post(info).await; 28 | } else { 29 | tokio::time::sleep(time::Duration::from_secs(3)).await; 30 | } 31 | } 32 | } 33 | 34 | async fn down_post(model: dl_post::Model) { 35 | match down_image(model.file_url.clone()).await { 36 | Ok((bytes, format)) => { 37 | let local_path = format!("{}.{}", model.dl_key, format); 38 | let abs_path = join_paths(vec![get_downloads_dir().as_str(), &local_path]); 39 | tokio::fs::write(&abs_path, &bytes).await.unwrap(); // todo: this is file system error, need stop download 40 | let _ = delete_download_info(model.dl_key).await; 41 | } 42 | Err(err) => { 43 | println!("{:?}", err); 44 | dl_post::update_status(model.dl_key, 2).await.unwrap(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /native/src/entities.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use serde_derive::Serialize; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 5 | pub struct TagNetworkResponse { 6 | pub version: i64, 7 | pub data: String, 8 | } 9 | 10 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 11 | pub struct TagData { 12 | pub version: i64, 13 | pub tags: Vec, 14 | } 15 | 16 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 17 | pub struct Tag { 18 | pub image_total: i64, 19 | pub tag_names: Vec, 20 | } 21 | 22 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 23 | pub struct PostPage { 24 | pub posts: Vec, 25 | pub page_total: i64, 26 | } 27 | 28 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 29 | pub struct Post { 30 | pub id: i64, 31 | pub tags: String, 32 | pub created_at: i64, 33 | pub creator_id: Option, 34 | pub author: String, 35 | pub change: i64, 36 | pub source: String, 37 | pub score: i64, 38 | pub md5: String, 39 | pub file_size: i64, 40 | pub file_url: String, 41 | pub is_shown_in_index: bool, 42 | pub preview_url: String, 43 | pub preview_width: i64, 44 | pub preview_height: i64, 45 | pub actual_preview_width: i64, 46 | pub actual_preview_height: i64, 47 | pub sample_url: String, 48 | pub sample_width: i64, 49 | pub sample_height: i64, 50 | pub sample_file_size: i64, 51 | pub jpeg_url: String, 52 | pub jpeg_width: i64, 53 | pub jpeg_height: i64, 54 | pub jpeg_file_size: i64, 55 | pub rating: String, 56 | pub has_children: bool, 57 | pub parent_id: Option, 58 | pub status: String, 59 | pub width: i64, 60 | pub height: i64, 61 | pub is_held: bool, 62 | pub frames_pending_string: String, 63 | // pub frames_pending: Vec, 64 | pub frames_string: String, 65 | // pub frames: Vec, 66 | } 67 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::sync::Mutex; 3 | 4 | pub use anyhow::Result; 5 | use lazy_static::lazy_static; 6 | use once_cell::sync::OnceCell; 7 | use tokio::runtime; 8 | 9 | use local::join_paths; 10 | 11 | use crate::database::init_database; 12 | use crate::local::create_dir_if_not_exists; 13 | 14 | mod api; 15 | mod bridge_generated; 16 | mod database; 17 | 18 | mod local; 19 | mod utils; 20 | 21 | mod download; 22 | mod entities; 23 | 24 | #[cfg(test)] 25 | mod tests; 26 | 27 | lazy_static! { 28 | pub(crate) static ref RUNTIME: runtime::Runtime = runtime::Builder::new_multi_thread() 29 | .enable_all() 30 | .thread_keep_alive(tokio::time::Duration::new(60, 0)) 31 | .max_blocking_threads(30) 32 | .worker_threads(30) 33 | .build() 34 | .unwrap(); 35 | static ref INIT_ED: Mutex = Mutex::new(false); 36 | } 37 | 38 | pub(crate) fn block_on(f: impl Future) -> T { 39 | RUNTIME.block_on(f) 40 | } 41 | 42 | static ROOT: OnceCell = OnceCell::new(); 43 | static DOWNLOADS_DIR: OnceCell = OnceCell::new(); 44 | static IMAGE_CACHE_DIR: OnceCell = OnceCell::new(); 45 | static DATABASE_DIR: OnceCell = OnceCell::new(); 46 | 47 | pub fn init_root(root: &str, downloads_to: &str) { 48 | let mut lock = INIT_ED.lock().unwrap(); 49 | if *lock { 50 | return; 51 | } 52 | *lock = true; 53 | println!( 54 | "Init application with root : {}, downloads_to : {}", 55 | root, downloads_to, 56 | ); 57 | ROOT.set(root.to_owned()).unwrap(); 58 | DOWNLOADS_DIR.set(downloads_to.to_owned()).unwrap(); 59 | IMAGE_CACHE_DIR 60 | .set(join_paths(vec![root, "image_cache"])) 61 | .unwrap(); 62 | DATABASE_DIR 63 | .set(join_paths(vec![root, "database"])) 64 | .unwrap(); 65 | create_dir_if_not_exists(ROOT.get().unwrap()); 66 | create_dir_if_not_exists(DOWNLOADS_DIR.get().unwrap()); 67 | create_dir_if_not_exists(IMAGE_CACHE_DIR.get().unwrap()); 68 | create_dir_if_not_exists(DATABASE_DIR.get().unwrap()); 69 | RUNTIME.block_on(init_database()); 70 | let _ = RUNTIME.spawn(download::download_demon()); 71 | } 72 | 73 | #[allow(dead_code)] 74 | pub(crate) fn get_root() -> &'static String { 75 | ROOT.get().unwrap() 76 | } 77 | 78 | pub(crate) fn get_downloads_dir() -> &'static String { 79 | DOWNLOADS_DIR.get().unwrap() 80 | } 81 | 82 | pub(crate) fn get_image_cache_dir() -> &'static String { 83 | IMAGE_CACHE_DIR.get().unwrap() 84 | } 85 | 86 | pub(crate) fn get_database_dir() -> &'static String { 87 | DATABASE_DIR.get().unwrap() 88 | } 89 | -------------------------------------------------------------------------------- /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).expect(format!("create dir {} failed", path).as_str()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /native/src/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test1() -> anyhow::Result<()> { 3 | crate::api::init("/tmp/coco".to_owned()); 4 | crate::api::set_host("https://konachan.net".to_owned()); 5 | println!("{}", serde_json::to_string(&crate::api::tag_summary()?)?); 6 | Ok(()) 7 | } 8 | 9 | #[test] 10 | fn test2() -> anyhow::Result<()> { 11 | crate::api::init("/tmp/coco".to_owned()); 12 | crate::api::set_host("https://konachan.net".to_owned()); 13 | println!( 14 | "{}", 15 | serde_json::to_string(&crate::api::load_posts("".to_string(), 1)?)? 16 | ); 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /native/src/utils.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::collections::hash_map::DefaultHasher; 3 | use std::hash::Hasher; 4 | use tokio::sync::{Mutex, MutexGuard}; 5 | 6 | lazy_static! { 7 | static ref HASH_LOCK: Vec> = { 8 | let mut mutex_vec: Vec> = vec![]; 9 | for _ in 0..64 { 10 | mutex_vec.push(Mutex::<()>::new(())); 11 | } 12 | mutex_vec 13 | }; 14 | } 15 | 16 | pub(crate) async fn hash_lock(url: &String) -> MutexGuard<'static, ()> { 17 | let mut s = DefaultHasher::new(); 18 | s.write(url.as_bytes()); 19 | HASH_LOCK[s.finish() as usize % HASH_LOCK.len()] 20 | .lock() 21 | .await 22 | } 23 | 24 | pub fn from_str serde::Deserialize<'de>>(json: &str) -> anyhow::Result { 25 | Ok(serde_path_to_error::deserialize( 26 | &mut serde_json::Deserializer::from_str(json), 27 | )?) 28 | } 29 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: coco 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: 1.54.1 17 | url_launcher: ^6.0.20 18 | flutter_styled_toast: ^2.0.0 19 | waterfall_flow: ^3.0.2 20 | photo_view: ^0.13.0 21 | decorated_icon: ^1.2.1 22 | modal_bottom_sheet: ^3.0.0-pre 23 | event: ^2.1.2 24 | share_plus: ^4.0.9 25 | ffi: ^2.0.1 26 | # file_picker: 5.2.11 27 | permission_handler: ^10.2.0 28 | 29 | dev_dependencies: 30 | flutter_test: 31 | sdk: flutter 32 | flutter_lints: ^1.0.0 33 | ffigen: 7.2.11 34 | 35 | flutter: 36 | generate: true 37 | uses-material-design: true 38 | assets: 39 | - lib/assets/ 40 | -------------------------------------------------------------------------------- /scripts/before-ipa.sh: -------------------------------------------------------------------------------- 1 | /usr/libexec/PlistBuddy -c 'Add :application-identifier string niuhuan.coco' ios/Runner/Info.plist 2 | -------------------------------------------------------------------------------- /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 | flutter build apk --target-platform android-arm64 12 | -------------------------------------------------------------------------------- /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 | 13 | -------------------------------------------------------------------------------- /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/build-ipa.sh: -------------------------------------------------------------------------------- 1 | # 构建未签名的IPA 2 | 3 | cd "$( cd "$( dirname "$0" )" && pwd )/.." 4 | 5 | 6 | touch native/src/bridge_generated.rs 7 | flutter_rust_bridge_codegen \ 8 | --rust-input native/src/api.rs \ 9 | --dart-output lib/bridge_generated.dart \ 10 | --c-output ios/Runner/bridge_generated.h \ 11 | --rust-crate-dir native \ 12 | --llvm-path $LLVM_HOME \ 13 | --class-name Native 14 | 15 | cargo build --manifest-path native/Cargo.toml --release --features= --lib --target=aarch64-apple-ios 16 | 17 | cp native/target/aarch64-apple-ios/release/libnative.a ios/Runner/ 18 | 19 | flutter build ios --release --no-codesign 20 | 21 | cd build 22 | mkdir -p Payload 23 | mv ios/iphoneos/Runner.app Payload 24 | 25 | sh ../scripts/thin-payload.sh 26 | zip -9 nosign.ipa -r Payload 27 | -------------------------------------------------------------------------------- /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(coco LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "coco") 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", "coco" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "coco" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 niuhuan. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "coco.exe" "\0" 98 | VALUE "ProductName", "coco" "\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"coco", 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/coco/deaaa8d79f247f57b64d73c8d26536b9da538428/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 | --------------------------------------------------------------------------------