commit df4d38012bcaed91ed4bdd9097a4fd05e9df9079 Author: chen08209 Date: Tue Apr 30 23:38:49 2024 +0800 Initial commit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7caffd7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,117 @@ +name: build + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - platform: android + os: ubuntu-latest + - platform: windows + os: windows-latest + - platform: linux + os: ubuntu-latest + - platform: macos + os: macos-13 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup JAVA + if: startsWith(matrix.platform,'android') + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Setup NDK + if: startsWith(matrix.platform,'android') + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r26b + add-to-path: true + link-to-sdk: true + + - name: Setup Android Signing + if: startsWith(matrix.platform,'android') + run: | + echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks + echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties + echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties + echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties + + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'core/go.mod' + cache-dependency-path: | + core/go.sum + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.x' + channel: 'stable' + cache: true + + - name: Get Flutter Dependency + run: flutter pub get + + - name: Setup + run: | + dart setup.dart ${{ matrix.platform }} + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ matrix.platform }} + path: ./dist + retention-days: 1 + overwrite: true + + + upload-release: + permissions: write-all + needs: [ build ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download + uses: actions/download-artifact@v4 + with: + path: ./dist/ + pattern: artifact-* + merge-multiple: true + + - name: Pre Release + run: | + pip install gitchangelog pystache mustache markdown + prelease=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "") + if [ -z "$prelease" ]; then + echo "init" > release.md + else + current="${{ github.ref_name }}" + echo -e "\n\n
All changes from $current to the latest commit:\n\n" >> release.md + gitchangelog "${prelease}.." >> release.md 2>&1 || echo "Error in gitchangelog" + echo -e "\n\n
" >> release.md + fi + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: ./dist/* + body_path: './release.md' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a1d002 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + + +#libclash +/libclash/ + +#jniLibs +/android/app/src/main/jniLibs/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7ad77e0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "core/Clash.Meta"] + path = core/Clash.Meta + url = git@github.com:chen08209/Clash.Meta.git + branch = FlClash +[submodule "plugins/flutter_distributor"] + path = plugins/flutter_distributor + url = git@github.com:chen08209/flutter_distributor.git + branch = main diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..30d90a7 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 796c8ef79279f9c774545b3771238c3098dbefab + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: android + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: ios + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: linux + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: macos + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: web + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: windows + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..441648f --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +
+ +[**简体中文**](README_zh_CN.md) + +
+ +## FlClash + +A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. + +on Desktop: +

+ +

+ +on Mobile: +

+ +

+ +## Features + +✈️ Multi-platform: Android, Windows, macOS and Linux + +💻 Adaptive multiple screen sizes, Multiple color themes available + +💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI + +✨ Support subscription link, Dark mode + +## Build + +1. Update submodules + ```bash + git submodule update --init --recursive + ``` + +2. Install `Flutter` and `Golang` environment + +3. Build Application + + - android + + 1. Install `Android SDK` , `Android NDK` + + 2. Set `ANDROID_NDK` environment variables + + 3. Run Build script + + ```bash + dart .\setup.dart android + ``` + + - windows + + 1. You need a windows client + + 2. Install `Gcc`,`Inno Setup` + + 3. Run build script + + ```bash + dart .\setup.dart + ``` + + - linux + + 1. You need a linux client + + 2. Run build script + + ```bash + dart .\setup.dart + ``` + + - macOS + + 1. You need a macOS client + + 2. Run build script + + ```bash + dart .\setup.dart + ``` \ No newline at end of file diff --git a/README_zh_CN.md b/README_zh_CN.md new file mode 100644 index 0000000..e4ac90a --- /dev/null +++ b/README_zh_CN.md @@ -0,0 +1,84 @@ +
+ +[**English**](README.md) + +
+ +## FlClash + +基于ClashMeta的多平台代理客户端,简单易用,开源无广告。 + +on Desktop: +

+ +

+ +on Mobile: +

+ +

+ +## Features + +✈️ 多平台: Android, Windows, macOS and Linux + +💻 自适应多个屏幕尺寸,多种颜色主题可供选择 + +💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面 + +✨ 支持一键导入订阅, 深色模式 + +## Build + +1. 更新 submodules + ```bash + git submodule update --init --recursive + ``` + +2. 安装 `Flutter` 以及 `Golang` 环境 + +3. 构建应用 + + - android + + 1. 安装 `Android SDK` , `Android NDK` + + 2. 设置 `ANDROID_NDK` 环境变量 + + 3. 运行构建脚本 + + ```bash + dart .\setup.dart android + ``` + + - windows + + 1. 你需要一个windows客户端 + + 2. 安装 `Gcc`,`Inno Setup` + + 3. 运行构建脚本 + + ```bash + dart .\setup.dart + ``` + + - linux + + 1. 你需要一个linux客户端 + + 2. 运行构建脚本 + + ```bash + dart .\setup.dart + ``` + + - macOS + + 1. 你需要一个macOS客户端 + + 2. 运行构建脚本 + + ```bash + dart .\setup.dart + ``` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..c86cd99 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,112 @@ +import com.android.build.gradle.tasks.MergeSourceSetFolders + +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def defStoreFile = file("keystore.jks") +def defStorePassword = localProperties.getProperty('storePassword') +def defKeyAlias = localProperties.getProperty('keyAlias') +def defKeyPassword = localProperties.getProperty('keyPassword') +def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias != null && defKeyPassword != null + +android { + namespace "com.follow.clash" + compileSdkVersion 34 + ndkVersion "25.1.8937393" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + signingConfigs { + if (isRelease){ + release { + storeFile defStoreFile + storePassword defStorePassword + keyAlias defKeyAlias + keyPassword defKeyPassword + } + } + } + + + defaultConfig { + applicationId "com.follow.clash" + minSdkVersion 21 + targetSdkVersion 34 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + debug { + minifyEnabled false + applicationIdSuffix '.debug' + } + release { + minifyEnabled true + if(isRelease){ + signingConfig signingConfigs.release + }else{ + signingConfig signingConfigs.debug + } + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + } + } +} + +tasks.register('copyNativeLibs', Copy) { + delete('src/main/jniLibs') + from('../../libclash/android') + into('src/main/jniLibs') +} + +tasks.withType(MergeSourceSetFolders).configureEach { + dependsOn copyNativeLibs +} + +flutter { + source '../..' +} + +dependencies { + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation 'com.google.code.gson:gson:2.10' +} + + +afterEvaluate { + assembleDebug.dependsOn copyNativeLibs + + assembleRelease.dependsOn copyNativeLibs +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..17e22a6 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,2 @@ + +-keep class com.follow.clash.models.**{ *; } \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..74ddb95 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a7693a8 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..a1b0d62 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt new file mode 100644 index 0000000..f7b609e --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -0,0 +1,24 @@ +package com.follow.clash + +import androidx.lifecycle.MutableLiveData +import com.follow.clash.plugins.TilePlugin +import io.flutter.embedding.engine.FlutterEngine +import java.util.Date + +enum class RunState { + START, + PENDING, + STOP +} + +class GlobalState { + companion object { + val runState: MutableLiveData = MutableLiveData(RunState.STOP) + var runTime: Date? = null + var flutterEngine: FlutterEngine? = null + fun getCurrentTilePlugin(): TilePlugin? = + flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? + } +} + + diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt new file mode 100644 index 0000000..bd50766 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -0,0 +1,24 @@ +package com.follow.clash + + +import com.follow.clash.plugins.AppPlugin +import com.follow.clash.plugins.ProxyPlugin +import com.follow.clash.plugins.TilePlugin +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +class MainActivity : FlutterActivity() { + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + flutterEngine.plugins.add(AppPlugin()) + flutterEngine.plugins.add(ProxyPlugin()) + flutterEngine.plugins.add(TilePlugin()) + GlobalState.flutterEngine = flutterEngine + } + + override fun onDestroy() { + GlobalState.flutterEngine = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt new file mode 100644 index 0000000..6cc287c --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt @@ -0,0 +1,11 @@ +package com.follow.clash + +import android.app.Activity +import android.os.Bundle + +class TempActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + finishAndRemoveTask() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt new file mode 100644 index 0000000..5defdfd --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Drawable.kt @@ -0,0 +1,31 @@ +package com.follow.clash.extensions + +import java.net.InetSocketAddress + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.system.OsConstants.IPPROTO_TCP +import android.system.OsConstants.IPPROTO_UDP +import android.util.Base64 +import androidx.core.graphics.drawable.toBitmap +import com.follow.clash.models.Metadata +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream + + +suspend fun Drawable.getBase64(): String { + val drawable = this + return withContext(Dispatchers.IO) { + val bitmap = drawable.toBitmap() + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) + Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP) + } +} + +fun Metadata.getProtocol(): Int? { + if (network.startsWith("tcp")) return IPPROTO_TCP + if (network.startsWith("udp")) return IPPROTO_UDP + return null +} diff --git a/android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt b/android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt new file mode 100644 index 0000000..60d72f5 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt @@ -0,0 +1,12 @@ +package com.follow.clash.models + +enum class AccessControlMode { + acceptSelected, + rejectSelected, +} + +data class AccessControl( + val mode: AccessControlMode, + val acceptList: List, + val rejectList: List, +) diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Metadata.kt b/android/app/src/main/kotlin/com/follow/clash/models/Metadata.kt new file mode 100644 index 0000000..e7193d5 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/models/Metadata.kt @@ -0,0 +1,11 @@ +package com.follow.clash.models + +data class Metadata( + val network: String, + val sourceIP: String, + val sourcePort: Int, + val destinationIP: String, + val destinationPort: Int, + val remoteDestination: String, + val host: String +) diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Package.kt b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt new file mode 100644 index 0000000..41c8731 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt @@ -0,0 +1,7 @@ +package com.follow.clash.models + +data class Package( + val packageName: String, + val label: String, + val isSystem:Boolean +) diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt new file mode 100644 index 0000000..edc6355 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -0,0 +1,182 @@ +package com.follow.clash.plugins + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import com.follow.clash.extensions.getBase64 +import com.follow.clash.extensions.getProtocol +import com.follow.clash.models.Metadata +import com.follow.clash.models.Package +import com.google.gson.Gson +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.InetSocketAddress + +class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { + + private var activity: Activity? = null + + private var toast: Toast? = null + + private var context: Context? = null + + private lateinit var channel: MethodChannel + + private lateinit var scope: CoroutineScope + + private var connectivity: ConnectivityManager? = null + + private val iconMap = mutableMapOf() + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext; + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") + channel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + private fun tip(message: String?) { + if (toast != null) { + toast!!.cancel() + } + toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast!!.show() + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "moveTaskToBack" -> { + activity?.moveTaskToBack(true) + result.success(true); + } + + "getPackages" -> { + scope.launch { + result.success(getPackages()) + } + } + + "getPackageIcon" -> { + scope.launch { + val packageName = call.argument("packageName") + if (packageName != null) { + result.success(getPackageIcon(packageName)) + } else { + result.success(null) + } + } + } + + "getPackageName" -> { + val data = call.argument("data") + val metadata = + if (data != null) Gson().fromJson( + data, + Metadata::class.java + ) else null + val protocol = metadata?.getProtocol() + if (protocol == null) { + result.success(null) + return + } + scope.launch { + withContext(Dispatchers.Default) { + if (context == null) result.success(null) + val source = InetSocketAddress(metadata.sourceIP, metadata.sourcePort) + val target = InetSocketAddress( + metadata.host.ifEmpty { metadata.destinationIP }, + metadata.destinationPort + ) + if (connectivity == null) { + connectivity = context!!.getSystemService() + } + val uid = + connectivity?.getConnectionOwnerUid(protocol, source, target) + if (uid == null || uid == -1) { + result.success(null) + return@withContext + } + val packages = context?.packageManager?.getPackagesForUid(uid) + result.success(packages?.first()) + } + } + } + + "tip" -> { + val message = call.argument("message") + tip(message) + result.success(true) + + } + + else -> { + result.notImplemented(); + } + } + } + + private suspend fun getPackageIcon(packageName: String): String? { + val packageManager = context?.packageManager + if (iconMap[packageName] == null) { + iconMap[packageName] = packageManager?.getApplicationIcon(packageName)?.getBase64() + } + return iconMap[packageName] + } + + private suspend fun getPackages(): String { + return withContext(Dispatchers.Default){ + val packageManager = context?.packageManager + val packages: List? = + packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { + it.packageName == context?.packageName + || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == false + || it.packageName != "android" + + }?.map { + Package( + packageName = it.packageName, + label = it.applicationInfo.loadLabel(packageManager).toString(), + isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1 + ) + } + Gson().toJson(packages) + } + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity; + scope = CoroutineScope(Dispatchers.Default) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null; + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity; + } + + override fun onDetachedFromActivity() { + channel.invokeMethod("exit", null) + scope.cancel() + activity = null; + } +} diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt new file mode 100644 index 0000000..83ccf4d --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt @@ -0,0 +1,227 @@ +package com.follow.clash.plugins + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.net.VpnService +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.IBinder +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.follow.clash.GlobalState +import com.follow.clash.RunState +import com.follow.clash.models.AccessControl +import com.follow.clash.services.FlClashVpnService +import com.google.gson.Gson +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.util.Date + + +class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { + + + val VPN_PERMISSION_REQUEST_CODE = 1001 + val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002 + + private lateinit var flutterMethodChannel: MethodChannel + + private var activity: Activity? = null + private var context: Context? = null + private var flClashVpnService: FlClashVpnService? = null + private var isBound = false + private var port: Int? = null + private var accessControl: AccessControl? = null + private lateinit var title: String + private lateinit var content: String + + private val connection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as FlClashVpnService.LocalBinder + flClashVpnService = binder.getService() + port?.let { startVpn(it) } + isBound = true + } + + override fun onServiceDisconnected(arg0: ComponentName) { + flClashVpnService = null + isBound = false + } + } + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy") + flutterMethodChannel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + flutterMethodChannel.setMethodCallHandler(null) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { + "StartProxy" -> { + port = call.argument("port") + val args = call.argument("args") + accessControl = + if (args != null) Gson().fromJson(args, AccessControl::class.java) else null + handleStartVpn() + result.success(true) + } + + "StopProxy" -> { + stopVpn() + result.success(true) + } + + "SetProtect" -> { + val fd = call.argument("fd") + if (fd != null) { + flClashVpnService?.protect(fd) + result.success(true) + } else { + result.success(false) + } + } + + "GetRunTimeStamp" -> { + result.success(GlobalState.runTime?.time) + } + + "startForeground" -> { + title = call.argument("title") as String + content = call.argument("content") as String + requestNotificationsPermission() + result.success(true) + } + + else -> { + result.notImplemented() + } + } + + private fun handleStartVpn() { + val intent = VpnService.prepare(context) + if (intent != null) { + activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) + } else { + bindService() + } + } + + private fun startVpn(port: Int) { + if (GlobalState.runState.value == RunState.START) return; + flClashVpnService?.start(port, accessControl) + GlobalState.runState.value = RunState.START + GlobalState.runTime = Date() + startAfter() + } + + private fun stopVpn() { + if (GlobalState.runState.value == RunState.STOP) return + flClashVpnService?.stop() + unbindService() + GlobalState.runState.value = RunState.STOP; + GlobalState.runTime = null; + } + + @SuppressLint("ForegroundServiceType") + private fun startForeground() { + if (GlobalState.runState.value != RunState.START) return + flClashVpnService?.startForeground(title, content) + } + + private fun requestNotificationsPermission() { + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + val permission = context?.let { + ContextCompat.checkSelfPermission( + it, + Manifest.permission.POST_NOTIFICATIONS + ) + } + if (permission == PackageManager.PERMISSION_GRANTED) { + startForeground() + } else { + activity?.let { + ActivityCompat.requestPermissions( + it, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + NOTIFICATION_PERMISSION_REQUEST_CODE + ) + } + } + } else { + startForeground() + } + } + + private fun startAfter() { + flutterMethodChannel.invokeMethod("startAfter", flClashVpnService?.fd) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addActivityResultListener(::onActivityResult) + binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) + } + + private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode == VPN_PERMISSION_REQUEST_CODE) { + if (resultCode == FlutterActivity.RESULT_OK) { + bindService() + } else { + stopVpn() + } + } + return true; + } + + private fun onRequestPermissionsResultListener( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startForeground() + } + } + return true; + } + + + override fun onDetachedFromActivityForConfigChanges() { + activity = null; + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivity() { + stopVpn() + activity = null + } + + private fun bindService() { + val intent = Intent(context, FlClashVpnService::class.java) + context?.bindService(intent, connection, Context.BIND_AUTO_CREATE) + } + + private fun unbindService() { + if (isBound) { + context?.unbindService(connection) + isBound = false + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt new file mode 100644 index 0000000..e6233b2 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt @@ -0,0 +1,37 @@ +package com.follow.clash.plugins + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin, + MethodChannel.MethodCallHandler { + + private lateinit var channel: MethodChannel + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile") + channel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + handleDetached() + channel.setMethodCallHandler(null) + } + + fun handleStart() { + onStart?.let { it() } + channel.invokeMethod("start", null) + } + + fun handleStop() { + channel.invokeMethod("stop", null) + onStop?.let { it() } + } + + private fun handleDetached() { + channel.invokeMethod("detached", null) + } + + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {} +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt new file mode 100644 index 0000000..cc80f19 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt @@ -0,0 +1,105 @@ +package com.follow.clash.services + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Intent +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import androidx.annotation.RequiresApi +import androidx.lifecycle.Observer +import com.follow.clash.GlobalState +import com.follow.clash.RunState +import com.follow.clash.TempActivity +import com.follow.clash.plugins.AppPlugin +import com.follow.clash.plugins.ProxyPlugin +import com.follow.clash.plugins.TilePlugin +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor + + +@RequiresApi(Build.VERSION_CODES.N) +class FlClashTileService : TileService() { + + private val observer = Observer { runState -> + updateTile(runState) + } + + private fun updateTile(runState: RunState) { + if (qsTile != null) { + qsTile.state = when (runState) { + RunState.START -> Tile.STATE_ACTIVE + RunState.PENDING -> Tile.STATE_UNAVAILABLE + RunState.STOP -> Tile.STATE_INACTIVE + } + qsTile.updateTile() + } + } + + + override fun onStartListening() { + super.onStartListening() + GlobalState.runState.value?.let { updateTile(it) } + GlobalState.runState.observeForever(observer) + } + + + @SuppressLint("StartActivityAndCollapseDeprecated") + private fun activityTransfer() { + val intent = Intent(this, TempActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (Build.VERSION.SDK_INT >= 34) { + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + startActivityAndCollapse(pendingIntent) + } else { + startActivityAndCollapse(intent) + } + } + + private var flutterEngine: FlutterEngine? = null; + + private fun initFlutterEngine() { + flutterEngine = FlutterEngine(this) + flutterEngine?.plugins?.add(ProxyPlugin()) + flutterEngine?.plugins?.add(TilePlugin()) + flutterEngine?.plugins?.add(AppPlugin()) + GlobalState.flutterEngine = flutterEngine + if (flutterEngine?.dartExecutor?.isExecutingDart != true) { + val vpnService = DartExecutor.DartEntrypoint( + FlutterInjector.instance().flutterLoader().findAppBundlePath(), + "vpnService" + ) + flutterEngine?.dartExecutor?.executeDartEntrypoint(vpnService) + } + } + + override fun onClick() { + super.onClick() + activityTransfer() + val currentTilePlugin = GlobalState.getCurrentTilePlugin() + if (GlobalState.runState.value == RunState.STOP) { + GlobalState.runState.value = RunState.PENDING + if(currentTilePlugin == null){ + initFlutterEngine() + }else{ + currentTilePlugin?.handleStart() + } + } else if(GlobalState.runState.value == RunState.START){ + GlobalState.runState.value = RunState.PENDING + currentTilePlugin?.handleStop() + } + + } + + + override fun onDestroy() { + GlobalState.runState.removeObserver(observer) + super.onDestroy() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt new file mode 100644 index 0000000..444a3c0 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -0,0 +1,166 @@ +package com.follow.clash.services + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.net.ProxyInfo +import android.net.VpnService +import android.os.Binder +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.IconCompat +import com.follow.clash.GlobalState +import com.follow.clash.MainActivity +import com.follow.clash.models.AccessControl +import com.follow.clash.models.AccessControlMode + + +class FlClashVpnService : VpnService() { + + + private val CHANNEL = "FlClash" + + var fd: Int? = null; + + private val passList = listOf( + "*zhihu.com", + "*zhimg.com", + "*jd.com", + "100ime-iat-api.xfyun.cn", + "*360buyimg.com", + "localhost", + "*.local", + "127.*", + "10.*", + "172.16.*", + "172.17.*", + "172.18.*", + "172.19.*", + "172.2*", + "172.30.*", + "172.31.*", + "192.168.*" + ) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_STICKY + } + + fun start(port: Int, accessControl: AccessControl?) { + fd = with(Builder()) { + addAddress("172.16.0.1", 30) + setMtu(9000) + addRoute("0.0.0.0", 0) + if (accessControl != null) { + when (accessControl.mode) { + AccessControlMode.acceptSelected -> { + (accessControl.acceptList + packageName).forEach { + addAllowedApplication(it) + } + } + + AccessControlMode.rejectSelected -> { + (accessControl.rejectList - packageName).forEach { + addDisallowedApplication(it) + } + } + } + } + addDnsServer("172.16.0.2") + setSession("FlClash") + setBlocking(false) + if (Build.VERSION.SDK_INT >= 29) { + setMetered(false) + } + allowBypass() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setHttpProxy( + ProxyInfo.buildDirectProxy( + "127.0.0.1", + port, + passList + ) + ) + } + establish()?.detachFd() + } + } + + fun stop() { + stopSelf() + stopForeground() + } + + @SuppressLint("ForegroundServiceType", "WrongConstant") + fun startForeground(title: String, content: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_DEFAULT) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + + val intent = Intent(this, MainActivity::class.java) + val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } else { + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + val icon = IconCompat.createWithResource(this, this.applicationInfo.icon) + val notification = with(NotificationCompat.Builder(this, CHANNEL)) { + setSmallIcon(icon) + setContentTitle(title) + setContentText(content) + foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + setContentIntent(pendingIntent) + setOngoing(true) + setShowWhen(false) + build() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } else { + startForeground(1, notification) + } + } + } + private fun stopForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + stopForeground(Service.STOP_FOREGROUND_REMOVE) + } + } + + private val binder = LocalBinder() + + inner class LocalBinder : Binder() { + fun getService(): FlClashVpnService = this@FlClashVpnService + } + + override fun onBind(intent: Intent): IBinder { + return binder + } + + override fun onUnbind(intent: Intent?): Boolean { + GlobalState.getCurrentTilePlugin()?.handleStop(); + return super.onUnbind(intent) + } + + override fun onDestroy() { + stop() + super.onDestroy() + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/tile_icon.png b/android/app/src/main/res/drawable/tile_icon.png new file mode 100644 index 0000000..b8ce5ae Binary files /dev/null and b/android/app/src/main/res/drawable/tile_icon.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..e202e4b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..e202e4b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c86b31e Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..286417f Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..20bcbd4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..fcdf2a8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..fd40daf Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..ab2adf1 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..2aa4570 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ac146f9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..dc27b6b Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..d933821 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..8f7d717 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..0d7cf0d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..4f88b23 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..62216a3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..185c77b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values-night-v27/styles.xml b/android/app/src/main/res/values-night-v27/styles.xml new file mode 100644 index 0000000..50802ba --- /dev/null +++ b/android/app/src/main/res/values-night-v27/styles.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..fa77035 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v27/styles.xml b/android/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..f690aeb --- /dev/null +++ b/android/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..d37e588 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #EFEFEF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..2000dd1 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + FlClash + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..41b016a --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..8feab38 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,32 @@ +buildscript { + ext.kotlin_version = "${kotlin_version}" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:$agp_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..74e2e7b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true +kotlin_version=1.9.22 +agp_version=8.2.1 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d055c79 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip + diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..b0ffbea --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "$agp_version" apply false + id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false +} + +include ":app" diff --git a/assets/data/geoip.metadb b/assets/data/geoip.metadb new file mode 100644 index 0000000..bdc3408 Binary files /dev/null and b/assets/data/geoip.metadb differ diff --git a/assets/images/app_icon.ico b/assets/images/app_icon.ico new file mode 100644 index 0000000..3c59a7b Binary files /dev/null and b/assets/images/app_icon.ico differ diff --git a/assets/images/launch_icon.png b/assets/images/launch_icon.png new file mode 100644 index 0000000..86b78b6 Binary files /dev/null and b/assets/images/launch_icon.png differ diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..a3bc328 --- /dev/null +++ b/build.yaml @@ -0,0 +1,11 @@ +targets: + $default: + builders: + source_gen:combining_builder: + options: + build_extensions: + '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' + freezed: + options: + build_extensions: + '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.freezed.dart' \ No newline at end of file diff --git a/core/Clash.Meta b/core/Clash.Meta new file mode 160000 index 0000000..0096393 --- /dev/null +++ b/core/Clash.Meta @@ -0,0 +1 @@ +Subproject commit 0096393b3a75c2d3c5ef02ec45e6d86fac712b9e diff --git a/core/common.go b/core/common.go new file mode 100644 index 0000000..25ff458 --- /dev/null +++ b/core/common.go @@ -0,0 +1,182 @@ +package main + +import "C" +import ( + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/process" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/config" + "github.com/metacubex/mihomo/dns" + "github.com/metacubex/mihomo/hub/executor" + "github.com/metacubex/mihomo/listener" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/tunnel" + "os" + "os/exec" + "runtime" + "syscall" +) + +type GenerateConfigParams struct { + ProfilePath *string `json:"profile-path"` + Config *config.RawConfig `json:"config" ` + IsPatch *bool `json:"is-patch"` +} + +type ChangeProxyParams struct { + GroupName *string `json:"group-name"` + ProxyName *string `json:"proxy-name"` +} + +type TestDelayParams struct { + ProxyName string `json:"proxy-name"` + Timeout int64 `json:"timeout"` +} + +type Delay struct { + Name string `json:"name"` + Value int32 `json:"value"` +} + +type Process struct { + Uid uint32 `json:"uid"` + Network string `json:"network"` + Source string `json:"source"` + Target string `json:"target"` +} + +func restartExecutable(execPath string) { + var err error + executor.Shutdown() + if runtime.GOOS == "windows" { + cmd := exec.Command(execPath, os.Args[1:]...) + log.Infoln("restarting: %q %q", execPath, os.Args[1:]) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Start() + if err != nil { + log.Fatalln("restarting: %s", err) + } + + os.Exit(0) + } + + log.Infoln("restarting: %q %q", execPath, os.Args[1:]) + err = syscall.Exec(execPath, os.Args, os.Environ()) + if err != nil { + log.Fatalln("restarting: %s", err) + } +} + +func readFile(path string) ([]byte, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, err + } + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return data, err +} + +func getRawConfigWithPath(path *string) *config.RawConfig { + if path == nil { + return config.DefaultRawConfig() + } else { + bytes, err := readFile(*path) + if err != nil { + log.Errorln("getProfile readFile error %v", err) + return config.DefaultRawConfig() + } + prof, err := config.UnmarshalRawConfig(bytes) + if err != nil { + log.Errorln("getProfile UnmarshalRawConfig error %v", err) + return config.DefaultRawConfig() + } + return prof + } +} + +func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig { + prof := getRawConfigWithPath(profilePath) + overwriteConfig(prof, cfg) + return prof +} + +func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { + targetConfig.ExternalController = "" + targetConfig.ExternalUI = "" + targetConfig.Interface = "" + targetConfig.ExternalUIURL = "" + targetConfig.IPv6 = patchConfig.IPv6 + targetConfig.LogLevel = patchConfig.LogLevel + targetConfig.FindProcessMode = process.FindProcessAlways + targetConfig.AllowLan = patchConfig.AllowLan + targetConfig.MixedPort = patchConfig.MixedPort + targetConfig.Mode = patchConfig.Mode + targetConfig.Tun.Enable = patchConfig.Tun.Enable + targetConfig.Tun.Device = patchConfig.Tun.Device + targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack + targetConfig.Tun.Stack = patchConfig.Tun.Stack + if targetConfig.DNS.Enable == false { + targetConfig.DNS = patchConfig.DNS + } else { + targetConfig.DNS.UseHosts = patchConfig.DNS.UseHosts + targetConfig.DNS.EnhancedMode = patchConfig.DNS.EnhancedMode + targetConfig.DNS.IPv6 = patchConfig.DNS.IPv6 + targetConfig.DNS.DefaultNameserver = append(patchConfig.DNS.DefaultNameserver, targetConfig.DNS.DefaultNameserver...) + targetConfig.DNS.NameServer = append(patchConfig.DNS.NameServer, targetConfig.DNS.NameServer...) + targetConfig.DNS.FakeIPFilter = append(patchConfig.DNS.FakeIPFilter, targetConfig.DNS.FakeIPFilter...) + targetConfig.DNS.Fallback = append(patchConfig.DNS.Fallback, targetConfig.DNS.Fallback...) + if runtime.GOOS == "android" { + targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder) + } else if runtime.GOOS == "windows" { + targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder) + } + } +} + +func patchConfig(general *config.General) { + log.Infoln("[Apply] patch") + listener.SetAllowLan(general.AllowLan) + inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) + inbound.SetAllowedIPs(general.LanAllowedIPs) + inbound.SetDisAllowedIPs(general.LanDisAllowedIPs) + listener.SetBindAddress(general.BindAddress) + tunnel.SetSniffing(general.Sniffing) + dialer.SetTcpConcurrent(general.TCPConcurrent) + dialer.DefaultInterface.Store(general.Interface) + listener.ReCreateHTTP(general.Port, tunnel.Tunnel) + listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) + listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) + listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel) + listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) + listener.ReCreateTun(general.Tun, tunnel.Tunnel) + listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) + listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) + listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) + listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) + tunnel.SetMode(general.Mode) + log.SetLevel(general.LogLevel) + resolver.DisableIPv6 = !general.IPv6 +} + +func applyConfig(isPatch bool) bool { + if currentConfig == nil { + return false + } + cfg, err := config.ParseRawConfig(currentConfig) + if err != nil { + cfg, _ = config.ParseRawConfig(config.DefaultRawConfig()) + } + if isPatch { + patchConfig(cfg.General) + } else { + executor.ApplyConfig(cfg, true) + + } + return true +} diff --git a/core/dart-bridge/include/dart_api.h b/core/dart-bridge/include/dart_api.h new file mode 100644 index 0000000..99dde6f --- /dev/null +++ b/core/dart-bridge/include/dart_api.h @@ -0,0 +1,4185 @@ +/* + * Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_INCLUDE_DART_API_H_ +#define RUNTIME_INCLUDE_DART_API_H_ + +/** \mainpage Dart Embedding API Reference + * + * This reference describes the Dart Embedding API, which is used to embed the + * Dart Virtual Machine within C/C++ applications. + * + * This reference is generated from the header include/dart_api.h. + */ + +/* __STDC_FORMAT_MACROS has to be defined before including to + * enable platform independent printf format specifiers. */ +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS +#endif + +#include +#include +#include + +#if defined(__Fuchsia__) +#include +#endif + +#ifdef __cplusplus +#define DART_EXTERN_C extern "C" +#else +#define DART_EXTERN_C extern +#endif + +#if defined(__CYGWIN__) +#error Tool chain and platform not supported. +#elif defined(_WIN32) +#if defined(DART_SHARED_LIB) +#define DART_EXPORT DART_EXTERN_C __declspec(dllexport) +#else +#define DART_EXPORT DART_EXTERN_C +#endif +#else +#if __GNUC__ >= 4 +#if defined(DART_SHARED_LIB) +#define DART_EXPORT \ + DART_EXTERN_C __attribute__((visibility("default"))) __attribute((used)) +#else +#define DART_EXPORT DART_EXTERN_C +#endif +#else +#error Tool chain not supported. +#endif +#endif + +#if __GNUC__ +#define DART_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#elif _MSC_VER +#define DART_WARN_UNUSED_RESULT _Check_return_ +#else +#define DART_WARN_UNUSED_RESULT +#endif + +/* + * ======= + * Handles + * ======= + */ + +/** + * An isolate is the unit of concurrency in Dart. Each isolate has + * its own memory and thread of control. No state is shared between + * isolates. Instead, isolates communicate by message passing. + * + * Each thread keeps track of its current isolate, which is the + * isolate which is ready to execute on the current thread. The + * current isolate may be NULL, in which case no isolate is ready to + * execute. Most of the Dart apis require there to be a current + * isolate in order to function without error. The current isolate is + * set by any call to Dart_CreateIsolateGroup or Dart_EnterIsolate. + */ +typedef struct _Dart_Isolate* Dart_Isolate; +typedef struct _Dart_IsolateGroup* Dart_IsolateGroup; + +/** + * An object reference managed by the Dart VM garbage collector. + * + * Because the garbage collector may move objects, it is unsafe to + * refer to objects directly. Instead, we refer to objects through + * handles, which are known to the garbage collector and updated + * automatically when the object is moved. Handles should be passed + * by value (except in cases like out-parameters) and should never be + * allocated on the heap. + * + * Most functions in the Dart Embedding API return a handle. When a + * function completes normally, this will be a valid handle to an + * object in the Dart VM heap. This handle may represent the result of + * the operation or it may be a special valid handle used merely to + * indicate successful completion. Note that a valid handle may in + * some cases refer to the null object. + * + * --- Error handles --- + * + * When a function encounters a problem that prevents it from + * completing normally, it returns an error handle (See Dart_IsError). + * An error handle has an associated error message that gives more + * details about the problem (See Dart_GetError). + * + * There are four kinds of error handles that can be produced, + * depending on what goes wrong: + * + * - Api error handles are produced when an api function is misused. + * This happens when a Dart embedding api function is called with + * invalid arguments or in an invalid context. + * + * - Unhandled exception error handles are produced when, during the + * execution of Dart code, an exception is thrown but not caught. + * Prototypically this would occur during a call to Dart_Invoke, but + * it can occur in any function which triggers the execution of Dart + * code (for example, Dart_ToString). + * + * An unhandled exception error provides access to an exception and + * stacktrace via the functions Dart_ErrorGetException and + * Dart_ErrorGetStackTrace. + * + * - Compilation error handles are produced when, during the execution + * of Dart code, a compile-time error occurs. As above, this can + * occur in any function which triggers the execution of Dart code. + * + * - Fatal error handles are produced when the system wants to shut + * down the current isolate. + * + * --- Propagating errors --- + * + * When an error handle is returned from the top level invocation of + * Dart code in a program, the embedder must handle the error as they + * see fit. Often, the embedder will print the error message produced + * by Dart_Error and exit the program. + * + * When an error is returned while in the body of a native function, + * it can be propagated up the call stack by calling + * Dart_PropagateError, Dart_SetReturnValue, or Dart_ThrowException. + * Errors should be propagated unless there is a specific reason not + * to. If an error is not propagated then it is ignored. For + * example, if an unhandled exception error is ignored, that + * effectively "catches" the unhandled exception. Fatal errors must + * always be propagated. + * + * When an error is propagated, any current scopes created by + * Dart_EnterScope will be exited. + * + * Using Dart_SetReturnValue to propagate an exception is somewhat + * more convenient than using Dart_PropagateError, and should be + * preferred for reasons discussed below. + * + * Dart_PropagateError and Dart_ThrowException do not return. Instead + * they transfer control non-locally using a setjmp-like mechanism. + * This can be inconvenient if you have resources that you need to + * clean up before propagating the error. + * + * When relying on Dart_PropagateError, we often return error handles + * rather than propagating them from helper functions. Consider the + * following contrived example: + * + * 1 Dart_Handle isLongStringHelper(Dart_Handle arg) { + * 2 intptr_t* length = 0; + * 3 result = Dart_StringLength(arg, &length); + * 4 if (Dart_IsError(result)) { + * 5 return result; + * 6 } + * 7 return Dart_NewBoolean(length > 100); + * 8 } + * 9 + * 10 void NativeFunction_isLongString(Dart_NativeArguments args) { + * 11 Dart_EnterScope(); + * 12 AllocateMyResource(); + * 13 Dart_Handle arg = Dart_GetNativeArgument(args, 0); + * 14 Dart_Handle result = isLongStringHelper(arg); + * 15 if (Dart_IsError(result)) { + * 16 FreeMyResource(); + * 17 Dart_PropagateError(result); + * 18 abort(); // will not reach here + * 19 } + * 20 Dart_SetReturnValue(result); + * 21 FreeMyResource(); + * 22 Dart_ExitScope(); + * 23 } + * + * In this example, we have a native function which calls a helper + * function to do its work. On line 5, the helper function could call + * Dart_PropagateError, but that would not give the native function a + * chance to call FreeMyResource(), causing a leak. Instead, the + * helper function returns the error handle to the caller, giving the + * caller a chance to clean up before propagating the error handle. + * + * When an error is propagated by calling Dart_SetReturnValue, the + * native function will be allowed to complete normally and then the + * exception will be propagated only once the native call + * returns. This can be convenient, as it allows the C code to clean + * up normally. + * + * The example can be written more simply using Dart_SetReturnValue to + * propagate the error. + * + * 1 Dart_Handle isLongStringHelper(Dart_Handle arg) { + * 2 intptr_t* length = 0; + * 3 result = Dart_StringLength(arg, &length); + * 4 if (Dart_IsError(result)) { + * 5 return result + * 6 } + * 7 return Dart_NewBoolean(length > 100); + * 8 } + * 9 + * 10 void NativeFunction_isLongString(Dart_NativeArguments args) { + * 11 Dart_EnterScope(); + * 12 AllocateMyResource(); + * 13 Dart_Handle arg = Dart_GetNativeArgument(args, 0); + * 14 Dart_SetReturnValue(isLongStringHelper(arg)); + * 15 FreeMyResource(); + * 16 Dart_ExitScope(); + * 17 } + * + * In this example, the call to Dart_SetReturnValue on line 14 will + * either return the normal return value or the error (potentially + * generated on line 3). The call to FreeMyResource on line 15 will + * execute in either case. + * + * --- Local and persistent handles --- + * + * Local handles are allocated within the current scope (see + * Dart_EnterScope) and go away when the current scope exits. Unless + * otherwise indicated, callers should assume that all functions in + * the Dart embedding api return local handles. + * + * Persistent handles are allocated within the current isolate. They + * can be used to store objects across scopes. Persistent handles have + * the lifetime of the current isolate unless they are explicitly + * deallocated (see Dart_DeletePersistentHandle). + * The type Dart_Handle represents a handle (both local and persistent). + * The type Dart_PersistentHandle is a Dart_Handle and it is used to + * document that a persistent handle is expected as a parameter to a call + * or the return value from a call is a persistent handle. + * + * FinalizableHandles are persistent handles which are auto deleted when + * the object is garbage collected. It is never safe to use these handles + * unless you know the object is still reachable. + * + * WeakPersistentHandles are persistent handles which are automatically set + * to point Dart_Null when the object is garbage collected. They are not auto + * deleted, so it is safe to use them after the object has become unreachable. + */ +typedef struct _Dart_Handle* Dart_Handle; +typedef Dart_Handle Dart_PersistentHandle; +typedef struct _Dart_WeakPersistentHandle* Dart_WeakPersistentHandle; +typedef struct _Dart_FinalizableHandle* Dart_FinalizableHandle; +// These structs are versioned by DART_API_DL_MAJOR_VERSION, bump the +// version when changing this struct. + +typedef void (*Dart_HandleFinalizer)(void* isolate_callback_data, void* peer); + +/** + * Is this an error handle? + * + * Requires there to be a current isolate. + */ +DART_EXPORT bool Dart_IsError(Dart_Handle handle); + +/** + * Is this an api error handle? + * + * Api error handles are produced when an api function is misused. + * This happens when a Dart embedding api function is called with + * invalid arguments or in an invalid context. + * + * Requires there to be a current isolate. + */ +DART_EXPORT bool Dart_IsApiError(Dart_Handle handle); + +/** + * Is this an unhandled exception error handle? + * + * Unhandled exception error handles are produced when, during the + * execution of Dart code, an exception is thrown but not caught. + * This can occur in any function which triggers the execution of Dart + * code. + * + * See Dart_ErrorGetException and Dart_ErrorGetStackTrace. + * + * Requires there to be a current isolate. + */ +DART_EXPORT bool Dart_IsUnhandledExceptionError(Dart_Handle handle); + +/** + * Is this a compilation error handle? + * + * Compilation error handles are produced when, during the execution + * of Dart code, a compile-time error occurs. This can occur in any + * function which triggers the execution of Dart code. + * + * Requires there to be a current isolate. + */ +DART_EXPORT bool Dart_IsCompilationError(Dart_Handle handle); + +/** + * Is this a fatal error handle? + * + * Fatal error handles are produced when the system wants to shut down + * the current isolate. + * + * Requires there to be a current isolate. + */ +DART_EXPORT bool Dart_IsFatalError(Dart_Handle handle); + +/** + * Gets the error message from an error handle. + * + * Requires there to be a current isolate. + * + * \return A C string containing an error message if the handle is + * error. An empty C string ("") if the handle is valid. This C + * String is scope allocated and is only valid until the next call + * to Dart_ExitScope. +*/ +DART_EXPORT const char* Dart_GetError(Dart_Handle handle); + +/** + * Is this an error handle for an unhandled exception? + */ +DART_EXPORT bool Dart_ErrorHasException(Dart_Handle handle); + +/** + * Gets the exception Object from an unhandled exception error handle. + */ +DART_EXPORT Dart_Handle Dart_ErrorGetException(Dart_Handle handle); + +/** + * Gets the stack trace Object from an unhandled exception error handle. + */ +DART_EXPORT Dart_Handle Dart_ErrorGetStackTrace(Dart_Handle handle); + +/** + * Produces an api error handle with the provided error message. + * + * Requires there to be a current isolate. + * + * \param error the error message. + */ +DART_EXPORT Dart_Handle Dart_NewApiError(const char* error); +DART_EXPORT Dart_Handle Dart_NewCompilationError(const char* error); + +/** + * Produces a new unhandled exception error handle. + * + * Requires there to be a current isolate. + * + * \param exception An instance of a Dart object to be thrown or + * an ApiError or CompilationError handle. + * When an ApiError or CompilationError handle is passed in + * a string object of the error message is created and it becomes + * the Dart object to be thrown. + */ +DART_EXPORT Dart_Handle Dart_NewUnhandledExceptionError(Dart_Handle exception); + +/** + * Propagates an error. + * + * If the provided handle is an unhandled exception error, this + * function will cause the unhandled exception to be rethrown. This + * will proceed in the standard way, walking up Dart frames until an + * appropriate 'catch' block is found, executing 'finally' blocks, + * etc. + * + * If the error is not an unhandled exception error, we will unwind + * the stack to the next C frame. Intervening Dart frames will be + * discarded; specifically, 'finally' blocks will not execute. This + * is the standard way that compilation errors (and the like) are + * handled by the Dart runtime. + * + * In either case, when an error is propagated any current scopes + * created by Dart_EnterScope will be exited. + * + * See the additional discussion under "Propagating Errors" at the + * beginning of this file. + * + * \param handle An error handle (See Dart_IsError) + * + * On success, this function does not return. On failure, the + * process is terminated. + */ +DART_EXPORT void Dart_PropagateError(Dart_Handle handle); + +/** + * Converts an object to a string. + * + * May generate an unhandled exception error. + * + * \return The converted string if no error occurs during + * the conversion. If an error does occur, an error handle is + * returned. + */ +DART_EXPORT Dart_Handle Dart_ToString(Dart_Handle object); + +/** + * Checks to see if two handles refer to identically equal objects. + * + * If both handles refer to instances, this is equivalent to using the top-level + * function identical() from dart:core. Otherwise, returns whether the two + * argument handles refer to the same object. + * + * \param obj1 An object to be compared. + * \param obj2 An object to be compared. + * + * \return True if the objects are identically equal. False otherwise. + */ +DART_EXPORT bool Dart_IdentityEquals(Dart_Handle obj1, Dart_Handle obj2); + +/** + * Allocates a handle in the current scope from a persistent handle. + */ +DART_EXPORT Dart_Handle Dart_HandleFromPersistent(Dart_PersistentHandle object); + +/** + * Allocates a handle in the current scope from a weak persistent handle. + * + * This will be a handle to Dart_Null if the object has been garbage collected. + */ +DART_EXPORT Dart_Handle +Dart_HandleFromWeakPersistent(Dart_WeakPersistentHandle object); + +/** + * Allocates a persistent handle for an object. + * + * This handle has the lifetime of the current isolate unless it is + * explicitly deallocated by calling Dart_DeletePersistentHandle. + * + * Requires there to be a current isolate. + */ +DART_EXPORT Dart_PersistentHandle Dart_NewPersistentHandle(Dart_Handle object); + +/** + * Assign value of local handle to a persistent handle. + * + * Requires there to be a current isolate. + * + * \param obj1 A persistent handle whose value needs to be set. + * \param obj2 An object whose value needs to be set to the persistent handle. + */ +DART_EXPORT void Dart_SetPersistentHandle(Dart_PersistentHandle obj1, + Dart_Handle obj2); + +/** + * Deallocates a persistent handle. + * + * Requires there to be a current isolate group. + */ +DART_EXPORT void Dart_DeletePersistentHandle(Dart_PersistentHandle object); + +/** + * Allocates a weak persistent handle for an object. + * + * This handle has the lifetime of the current isolate. The handle can also be + * explicitly deallocated by calling Dart_DeleteWeakPersistentHandle. + * + * If the object becomes unreachable the callback is invoked with the peer as + * argument. The callback can be executed on any thread, will have a current + * isolate group, but will not have a current isolate. The callback can only + * call Dart_DeletePersistentHandle or Dart_DeleteWeakPersistentHandle. This + * gives the embedder the ability to cleanup data associated with the object. + * The handle will point to the Dart_Null object after the finalizer has been + * run. It is illegal to call into the VM with any other Dart_* functions from + * the callback. If the handle is deleted before the object becomes + * unreachable, the callback is never invoked. + * + * Requires there to be a current isolate. + * + * \param object An object with identity. + * \param peer A pointer to a native object or NULL. This value is + * provided to callback when it is invoked. + * \param external_allocation_size The number of externally allocated + * bytes for peer. Used to inform the garbage collector. + * \param callback A function pointer that will be invoked sometime + * after the object is garbage collected, unless the handle has been deleted. + * A valid callback needs to be specified it cannot be NULL. + * + * \return The weak persistent handle or NULL. NULL is returned in case of bad + * parameters. + */ +DART_EXPORT Dart_WeakPersistentHandle +Dart_NewWeakPersistentHandle(Dart_Handle object, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); + +/** + * Deletes the given weak persistent [object] handle. + * + * Requires there to be a current isolate group. + */ +DART_EXPORT void Dart_DeleteWeakPersistentHandle( + Dart_WeakPersistentHandle object); + +/** + * Updates the external memory size for the given weak persistent handle. + * + * May trigger garbage collection. + */ +DART_EXPORT void Dart_UpdateExternalSize(Dart_WeakPersistentHandle object, + intptr_t external_allocation_size); + +/** + * Allocates a finalizable handle for an object. + * + * This handle has the lifetime of the current isolate group unless the object + * pointed to by the handle is garbage collected, in this case the VM + * automatically deletes the handle after invoking the callback associated + * with the handle. The handle can also be explicitly deallocated by + * calling Dart_DeleteFinalizableHandle. + * + * If the object becomes unreachable the callback is invoked with the + * the peer as argument. The callback can be executed on any thread, will have + * an isolate group, but will not have a current isolate. The callback can only + * call Dart_DeletePersistentHandle or Dart_DeleteWeakPersistentHandle. + * This gives the embedder the ability to cleanup data associated with the + * object and clear out any cached references to the handle. All references to + * this handle after the callback will be invalid. It is illegal to call into + * the VM with any other Dart_* functions from the callback. If the handle is + * deleted before the object becomes unreachable, the callback is never + * invoked. + * + * Requires there to be a current isolate. + * + * \param object An object with identity. + * \param peer A pointer to a native object or NULL. This value is + * provided to callback when it is invoked. + * \param external_allocation_size The number of externally allocated + * bytes for peer. Used to inform the garbage collector. + * \param callback A function pointer that will be invoked sometime + * after the object is garbage collected, unless the handle has been deleted. + * A valid callback needs to be specified it cannot be NULL. + * + * \return The finalizable handle or NULL. NULL is returned in case of bad + * parameters. + */ +DART_EXPORT Dart_FinalizableHandle +Dart_NewFinalizableHandle(Dart_Handle object, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); + +/** + * Deletes the given finalizable [object] handle. + * + * The caller has to provide the actual Dart object the handle was created from + * to prove the object (and therefore the finalizable handle) is still alive. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_DeleteFinalizableHandle(Dart_FinalizableHandle object, + Dart_Handle strong_ref_to_object); + +/** + * Updates the external memory size for the given finalizable handle. + * + * The caller has to provide the actual Dart object the handle was created from + * to prove the object (and therefore the finalizable handle) is still alive. + * + * May trigger garbage collection. + */ +DART_EXPORT void Dart_UpdateFinalizableExternalSize( + Dart_FinalizableHandle object, + Dart_Handle strong_ref_to_object, + intptr_t external_allocation_size); + +/* + * ========================== + * Initialization and Globals + * ========================== + */ + +/** + * Gets the version string for the Dart VM. + * + * The version of the Dart VM can be accessed without initializing the VM. + * + * \return The version string for the embedded Dart VM. + */ +DART_EXPORT const char* Dart_VersionString(void); + +/** + * Isolate specific flags are set when creating a new isolate using the + * Dart_IsolateFlags structure. + * + * Current version of flags is encoded in a 32-bit integer with 16 bits used + * for each part. + */ + +#define DART_FLAGS_CURRENT_VERSION (0x0000000c) + +typedef struct { + int32_t version; + bool enable_asserts; + bool use_field_guards; + bool use_osr; + bool obfuscate; + bool load_vmservice_library; + bool copy_parent_code; + bool null_safety; + bool is_system_isolate; + bool snapshot_is_dontneed_safe; + bool branch_coverage; +} Dart_IsolateFlags; + +/** + * Initialize Dart_IsolateFlags with correct version and default values. + */ +DART_EXPORT void Dart_IsolateFlagsInitialize(Dart_IsolateFlags* flags); + +/** + * An isolate creation and initialization callback function. + * + * This callback, provided by the embedder, is called when the VM + * needs to create an isolate. The callback should create an isolate + * by calling Dart_CreateIsolateGroup and load any scripts required for + * execution. + * + * This callback may be called on a different thread than the one + * running the parent isolate. + * + * When the function returns NULL, it is the responsibility of this + * function to ensure that Dart_ShutdownIsolate has been called if + * required (for example, if the isolate was created successfully by + * Dart_CreateIsolateGroup() but the root library fails to load + * successfully, then the function should call Dart_ShutdownIsolate + * before returning). + * + * When the function returns NULL, the function should set *error to + * a malloc-allocated buffer containing a useful error message. The + * caller of this function (the VM) will make sure that the buffer is + * freed. + * + * \param script_uri The uri of the main source file or snapshot to load. + * Either the URI of the parent isolate set in Dart_CreateIsolateGroup for + * Isolate.spawn, or the argument to Isolate.spawnUri canonicalized by the + * library tag handler of the parent isolate. + * The callback is responsible for loading the program by a call to + * Dart_LoadScriptFromKernel. + * \param main The name of the main entry point this isolate will + * eventually run. This is provided for advisory purposes only to + * improve debugging messages. The main function is not invoked by + * this function. + * \param package_root Ignored. + * \param package_config Uri of the package configuration file (either in format + * of .packages or .dart_tool/package_config.json) for this isolate + * to resolve package imports against. If this parameter is not passed the + * package resolution of the parent isolate should be used. + * \param flags Default flags for this isolate being spawned. Either inherited + * from the spawning isolate or passed as parameters when spawning the + * isolate from Dart code. + * \param isolate_data The isolate data which was passed to the + * parent isolate when it was created by calling Dart_CreateIsolateGroup(). + * \param error A structure into which the embedder can place a + * C string containing an error message in the case of failures. + * + * \return The embedder returns NULL if the creation and + * initialization was not successful and the isolate if successful. + */ +typedef Dart_Isolate (*Dart_IsolateGroupCreateCallback)( + const char* script_uri, + const char* main, + const char* package_root, + const char* package_config, + Dart_IsolateFlags* flags, + void* isolate_data, + char** error); + +/** + * An isolate initialization callback function. + * + * This callback, provided by the embedder, is called when the VM has created an + * isolate within an existing isolate group (i.e. from the same source as an + * existing isolate). + * + * The callback should setup native resolvers and might want to set a custom + * message handler via [Dart_SetMessageNotifyCallback] and mark the isolate as + * runnable. + * + * This callback may be called on a different thread than the one + * running the parent isolate. + * + * When the function returns `false`, it is the responsibility of this + * function to ensure that `Dart_ShutdownIsolate` has been called. + * + * When the function returns `false`, the function should set *error to + * a malloc-allocated buffer containing a useful error message. The + * caller of this function (the VM) will make sure that the buffer is + * freed. + * + * \param child_isolate_data The callback data to associate with the new + * child isolate. + * \param error A structure into which the embedder can place a + * C string containing an error message in the case the initialization fails. + * + * \return The embedder returns true if the initialization was successful and + * false otherwise (in which case the VM will terminate the isolate). + */ +typedef bool (*Dart_InitializeIsolateCallback)(void** child_isolate_data, + char** error); + +/** + * An isolate shutdown callback function. + * + * This callback, provided by the embedder, is called before the vm + * shuts down an isolate. The isolate being shutdown will be the current + * isolate. It is safe to run Dart code. + * + * This function should be used to dispose of native resources that + * are allocated to an isolate in order to avoid leaks. + * + * \param isolate_group_data The same callback data which was passed to the + * isolate group when it was created. + * \param isolate_data The same callback data which was passed to the isolate + * when it was created. + */ +typedef void (*Dart_IsolateShutdownCallback)(void* isolate_group_data, + void* isolate_data); + +/** + * An isolate cleanup callback function. + * + * This callback, provided by the embedder, is called after the vm + * shuts down an isolate. There will be no current isolate and it is *not* + * safe to run Dart code. + * + * This function should be used to dispose of native resources that + * are allocated to an isolate in order to avoid leaks. + * + * \param isolate_group_data The same callback data which was passed to the + * isolate group when it was created. + * \param isolate_data The same callback data which was passed to the isolate + * when it was created. + */ +typedef void (*Dart_IsolateCleanupCallback)(void* isolate_group_data, + void* isolate_data); + +/** + * An isolate group cleanup callback function. + * + * This callback, provided by the embedder, is called after the vm + * shuts down an isolate group. + * + * This function should be used to dispose of native resources that + * are allocated to an isolate in order to avoid leaks. + * + * \param isolate_group_data The same callback data which was passed to the + * isolate group when it was created. + * + */ +typedef void (*Dart_IsolateGroupCleanupCallback)(void* isolate_group_data); + +/** + * A thread start callback function. + * This callback, provided by the embedder, is called after a thread in the + * vm thread pool starts. + * This function could be used to adjust thread priority or attach native + * resources to the thread. + */ +typedef void (*Dart_ThreadStartCallback)(void); + +/** + * A thread death callback function. + * This callback, provided by the embedder, is called before a thread in the + * vm thread pool exits. + * This function could be used to dispose of native resources that + * are associated and attached to the thread, in order to avoid leaks. + */ +typedef void (*Dart_ThreadExitCallback)(void); + +/** + * Opens a file for reading or writing. + * + * Callback provided by the embedder for file operations. If the + * embedder does not allow file operations this callback can be + * NULL. + * + * \param name The name of the file to open. + * \param write A boolean variable which indicates if the file is to + * opened for writing. If there is an existing file it needs to truncated. + */ +typedef void* (*Dart_FileOpenCallback)(const char* name, bool write); + +/** + * Read contents of file. + * + * Callback provided by the embedder for file operations. If the + * embedder does not allow file operations this callback can be + * NULL. + * + * \param data Buffer allocated in the callback into which the contents + * of the file are read into. It is the responsibility of the caller to + * free this buffer. + * \param file_length A variable into which the length of the file is returned. + * In the case of an error this value would be -1. + * \param stream Handle to the opened file. + */ +typedef void (*Dart_FileReadCallback)(uint8_t** data, + intptr_t* file_length, + void* stream); + +/** + * Write data into file. + * + * Callback provided by the embedder for file operations. If the + * embedder does not allow file operations this callback can be + * NULL. + * + * \param data Buffer which needs to be written into the file. + * \param length Length of the buffer. + * \param stream Handle to the opened file. + */ +typedef void (*Dart_FileWriteCallback)(const void* data, + intptr_t length, + void* stream); + +/** + * Closes the opened file. + * + * Callback provided by the embedder for file operations. If the + * embedder does not allow file operations this callback can be + * NULL. + * + * \param stream Handle to the opened file. + */ +typedef void (*Dart_FileCloseCallback)(void* stream); + +typedef bool (*Dart_EntropySource)(uint8_t* buffer, intptr_t length); + +/** + * Callback provided by the embedder that is used by the vmservice isolate + * to request the asset archive. The asset archive must be an uncompressed tar + * archive that is stored in a Uint8List. + * + * If the embedder has no vmservice isolate assets, the callback can be NULL. + * + * \return The embedder must return a handle to a Uint8List containing an + * uncompressed tar archive or null. + */ +typedef Dart_Handle (*Dart_GetVMServiceAssetsArchive)(void); + +/** + * The current version of the Dart_InitializeFlags. Should be incremented every + * time Dart_InitializeFlags changes in a binary incompatible way. + */ +#define DART_INITIALIZE_PARAMS_CURRENT_VERSION (0x00000008) + +/** Forward declaration */ +struct Dart_CodeObserver; + +/** + * Callback provided by the embedder that is used by the VM to notify on code + * object creation, *before* it is invoked the first time. + * This is useful for embedders wanting to e.g. keep track of PCs beyond + * the lifetime of the garbage collected code objects. + * Note that an address range may be used by more than one code object over the + * lifecycle of a process. Clients of this function should record timestamps for + * these compilation events and when collecting PCs to disambiguate reused + * address ranges. + */ +typedef void (*Dart_OnNewCodeCallback)(struct Dart_CodeObserver* observer, + const char* name, + uintptr_t base, + uintptr_t size); + +typedef struct Dart_CodeObserver { + void* data; + + Dart_OnNewCodeCallback on_new_code; +} Dart_CodeObserver; + +/** + * Optional callback provided by the embedder that is used by the VM to + * implement registration of kernel blobs for the subsequent Isolate.spawnUri + * If no callback is provided, the registration of kernel blobs will throw + * an error. + * + * \param kernel_buffer A buffer which contains a kernel program. Callback + * should copy the contents of `kernel_buffer` as + * it may be freed immediately after registration. + * \param kernel_buffer_size The size of `kernel_buffer`. + * + * \return A C string representing URI which can be later used + * to spawn a new isolate. This C String should be scope allocated + * or owned by the embedder. + * Returns NULL if embedder runs out of memory. + */ +typedef const char* (*Dart_RegisterKernelBlobCallback)( + const uint8_t* kernel_buffer, + intptr_t kernel_buffer_size); + +/** + * Optional callback provided by the embedder that is used by the VM to + * unregister kernel blobs. + * If no callback is provided, the unregistration of kernel blobs will throw + * an error. + * + * \param kernel_blob_uri URI of the kernel blob to unregister. + */ +typedef void (*Dart_UnregisterKernelBlobCallback)(const char* kernel_blob_uri); + +/** + * Describes how to initialize the VM. Used with Dart_Initialize. + */ +typedef struct { + /** + * Identifies the version of the struct used by the client. + * should be initialized to DART_INITIALIZE_PARAMS_CURRENT_VERSION. + */ + int32_t version; + + /** + * A buffer containing snapshot data, or NULL if no snapshot is provided. + * + * If provided, the buffer must remain valid until Dart_Cleanup returns. + */ + const uint8_t* vm_snapshot_data; + + /** + * A buffer containing a snapshot of precompiled instructions, or NULL if + * no snapshot is provided. + * + * If provided, the buffer must remain valid until Dart_Cleanup returns. + */ + const uint8_t* vm_snapshot_instructions; + + /** + * A function to be called during isolate group creation. + * See Dart_IsolateGroupCreateCallback. + */ + Dart_IsolateGroupCreateCallback create_group; + + /** + * A function to be called during isolate + * initialization inside an existing isolate group. + * See Dart_InitializeIsolateCallback. + */ + Dart_InitializeIsolateCallback initialize_isolate; + + /** + * A function to be called right before an isolate is shutdown. + * See Dart_IsolateShutdownCallback. + */ + Dart_IsolateShutdownCallback shutdown_isolate; + + /** + * A function to be called after an isolate was shutdown. + * See Dart_IsolateCleanupCallback. + */ + Dart_IsolateCleanupCallback cleanup_isolate; + + /** + * A function to be called after an isolate group is + * shutdown. See Dart_IsolateGroupCleanupCallback. + */ + Dart_IsolateGroupCleanupCallback cleanup_group; + + Dart_ThreadStartCallback thread_start; + Dart_ThreadExitCallback thread_exit; + Dart_FileOpenCallback file_open; + Dart_FileReadCallback file_read; + Dart_FileWriteCallback file_write; + Dart_FileCloseCallback file_close; + Dart_EntropySource entropy_source; + + /** + * A function to be called by the service isolate when it requires the + * vmservice assets archive. See Dart_GetVMServiceAssetsArchive. + */ + Dart_GetVMServiceAssetsArchive get_service_assets; + + bool start_kernel_isolate; + + /** + * An external code observer callback function. The observer can be invoked + * as early as during the Dart_Initialize() call. + */ + Dart_CodeObserver* code_observer; + + /** + * Kernel blob registration callback function. See Dart_RegisterKernelBlobCallback. + */ + Dart_RegisterKernelBlobCallback register_kernel_blob; + + /** + * Kernel blob unregistration callback function. See Dart_UnregisterKernelBlobCallback. + */ + Dart_UnregisterKernelBlobCallback unregister_kernel_blob; + +#if defined(__Fuchsia__) + /** + * The resource needed to use zx_vmo_replace_as_executable. Can be + * ZX_HANDLE_INVALID if the process has ambient-replace-as-executable or if + * executable memory is not needed (e.g., this is an AOT runtime). + */ + zx_handle_t vmex_resource; +#endif +} Dart_InitializeParams; + +/** + * Initializes the VM. + * + * \param params A struct containing initialization information. The version + * field of the struct must be DART_INITIALIZE_PARAMS_CURRENT_VERSION. + * + * \return NULL if initialization is successful. Returns an error message + * otherwise. The caller is responsible for freeing the error message. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_Initialize( + Dart_InitializeParams* params); + +/** + * Cleanup state in the VM before process termination. + * + * \return NULL if cleanup is successful. Returns an error message otherwise. + * The caller is responsible for freeing the error message. + * + * NOTE: This function must not be called on a thread that was created by the VM + * itself. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_Cleanup(void); + +/** + * Sets command line flags. Should be called before Dart_Initialize. + * + * \param argc The length of the arguments array. + * \param argv An array of arguments. + * + * \return NULL if successful. Returns an error message otherwise. + * The caller is responsible for freeing the error message. + * + * NOTE: This call does not store references to the passed in c-strings. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_SetVMFlags(int argc, + const char** argv); + +/** + * Returns true if the named VM flag is of boolean type, specified, and set to + * true. + * + * \param flag_name The name of the flag without leading punctuation + * (example: "enable_asserts"). + */ +DART_EXPORT bool Dart_IsVMFlagSet(const char* flag_name); + +/* + * ======== + * Isolates + * ======== + */ + +/** + * Creates a new isolate. The new isolate becomes the current isolate. + * + * A snapshot can be used to restore the VM quickly to a saved state + * and is useful for fast startup. If snapshot data is provided, the + * isolate will be started using that snapshot data. Requires a core snapshot or + * an app snapshot created by Dart_CreateSnapshot or + * Dart_CreatePrecompiledSnapshot* from a VM with the same version. + * + * Requires there to be no current isolate. + * + * \param script_uri The main source file or snapshot this isolate will load. + * The VM will provide this URI to the Dart_IsolateGroupCreateCallback when a + * child isolate is created by Isolate.spawn. The embedder should use a URI + * that allows it to load the same program into such a child isolate. + * \param name A short name for the isolate to improve debugging messages. + * Typically of the format 'foo.dart:main()'. + * \param isolate_snapshot_data Buffer containing the snapshot data of the + * isolate or NULL if no snapshot is provided. If provided, the buffer must + * remain valid until the isolate shuts down. + * \param isolate_snapshot_instructions Buffer containing the snapshot + * instructions of the isolate or NULL if no snapshot is provided. If + * provided, the buffer must remain valid until the isolate shuts down. + * \param flags Pointer to VM specific flags or NULL for default flags. + * \param isolate_group_data Embedder group data. This data can be obtained + * by calling Dart_IsolateGroupData and will be passed to the + * Dart_IsolateShutdownCallback, Dart_IsolateCleanupCallback, and + * Dart_IsolateGroupCleanupCallback. + * \param isolate_data Embedder data. This data will be passed to + * the Dart_IsolateGroupCreateCallback when new isolates are spawned from + * this parent isolate. + * \param error Returns NULL if creation is successful, an error message + * otherwise. The caller is responsible for calling free() on the error + * message. + * + * \return The new isolate on success, or NULL if isolate creation failed. + */ +DART_EXPORT Dart_Isolate +Dart_CreateIsolateGroup(const char* script_uri, + const char* name, + const uint8_t* isolate_snapshot_data, + const uint8_t* isolate_snapshot_instructions, + Dart_IsolateFlags* flags, + void* isolate_group_data, + void* isolate_data, + char** error); +/** + * Creates a new isolate inside the isolate group of [group_member]. + * + * Requires there to be no current isolate. + * + * \param group_member An isolate from the same group into which the newly created + * isolate should be born into. Other threads may not have entered / enter this + * member isolate. + * \param name A short name for the isolate for debugging purposes. + * \param shutdown_callback A callback to be called when the isolate is being + * shutdown (may be NULL). + * \param cleanup_callback A callback to be called when the isolate is being + * cleaned up (may be NULL). + * \param child_isolate_data The embedder-specific data associated with this isolate. + * \param error Set to NULL if creation is successful, set to an error + * message otherwise. The caller is responsible for calling free() on the + * error message. + * + * \return The newly created isolate on success, or NULL if isolate creation + * failed. + * + * If successful, the newly created isolate will become the current isolate. + */ +DART_EXPORT Dart_Isolate +Dart_CreateIsolateInGroup(Dart_Isolate group_member, + const char* name, + Dart_IsolateShutdownCallback shutdown_callback, + Dart_IsolateCleanupCallback cleanup_callback, + void* child_isolate_data, + char** error); + +/* TODO(turnidge): Document behavior when there is already a current + * isolate. */ + +/** + * Creates a new isolate from a Dart Kernel file. The new isolate + * becomes the current isolate. + * + * Requires there to be no current isolate. + * + * \param script_uri The main source file or snapshot this isolate will load. + * The VM will provide this URI to the Dart_IsolateGroupCreateCallback when a + * child isolate is created by Isolate.spawn. The embedder should use a URI that + * allows it to load the same program into such a child isolate. + * \param name A short name for the isolate to improve debugging messages. + * Typically of the format 'foo.dart:main()'. + * \param kernel_buffer A buffer which contains a kernel/DIL program. Must + * remain valid until isolate shutdown. + * \param kernel_buffer_size The size of `kernel_buffer`. + * \param flags Pointer to VM specific flags or NULL for default flags. + * \param isolate_group_data Embedder group data. This data can be obtained + * by calling Dart_IsolateGroupData and will be passed to the + * Dart_IsolateShutdownCallback, Dart_IsolateCleanupCallback, and + * Dart_IsolateGroupCleanupCallback. + * \param isolate_data Embedder data. This data will be passed to + * the Dart_IsolateGroupCreateCallback when new isolates are spawned from + * this parent isolate. + * \param error Returns NULL if creation is successful, an error message + * otherwise. The caller is responsible for calling free() on the error + * message. + * + * \return The new isolate on success, or NULL if isolate creation failed. + */ +DART_EXPORT Dart_Isolate +Dart_CreateIsolateGroupFromKernel(const char* script_uri, + const char* name, + const uint8_t* kernel_buffer, + intptr_t kernel_buffer_size, + Dart_IsolateFlags* flags, + void* isolate_group_data, + void* isolate_data, + char** error); +/** + * Shuts down the current isolate. After this call, the current isolate is NULL. + * Any current scopes created by Dart_EnterScope will be exited. Invokes the + * shutdown callback and any callbacks of remaining weak persistent handles. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_ShutdownIsolate(void); +/* TODO(turnidge): Document behavior when there is no current isolate. */ + +/** + * Returns the current isolate. Will return NULL if there is no + * current isolate. + */ +DART_EXPORT Dart_Isolate Dart_CurrentIsolate(void); + +/** + * Returns the callback data associated with the current isolate. This + * data was set when the isolate got created or initialized. + */ +DART_EXPORT void* Dart_CurrentIsolateData(void); + +/** + * Returns the callback data associated with the given isolate. This + * data was set when the isolate got created or initialized. + */ +DART_EXPORT void* Dart_IsolateData(Dart_Isolate isolate); + +/** + * Returns the current isolate group. Will return NULL if there is no + * current isolate group. + */ +DART_EXPORT Dart_IsolateGroup Dart_CurrentIsolateGroup(void); + +/** + * Returns the callback data associated with the current isolate group. This + * data was passed to the isolate group when it was created. + */ +DART_EXPORT void* Dart_CurrentIsolateGroupData(void); + +/** + * Gets an id that uniquely identifies current isolate group. + * + * It is the responsibility of the caller to free the returned ID. + */ +typedef int64_t Dart_IsolateGroupId; +DART_EXPORT Dart_IsolateGroupId Dart_CurrentIsolateGroupId(void); + +/** + * Returns the callback data associated with the specified isolate group. This + * data was passed to the isolate when it was created. + * The embedder is responsible for ensuring the consistency of this data + * with respect to the lifecycle of an isolate group. + */ +DART_EXPORT void* Dart_IsolateGroupData(Dart_Isolate isolate); + +/** + * Returns the debugging name for the current isolate. + * + * This name is unique to each isolate and should only be used to make + * debugging messages more comprehensible. + */ +DART_EXPORT Dart_Handle Dart_DebugName(void); + +/** + * Returns the debugging name for the current isolate. + * + * This name is unique to each isolate and should only be used to make + * debugging messages more comprehensible. + * + * The returned string is scope allocated and is only valid until the next call + * to Dart_ExitScope. + */ +DART_EXPORT const char* Dart_DebugNameToCString(void); + +/** + * Returns the ID for an isolate which is used to query the service protocol. + * + * It is the responsibility of the caller to free the returned ID. + */ +DART_EXPORT const char* Dart_IsolateServiceId(Dart_Isolate isolate); + +/** + * Enters an isolate. After calling this function, + * the current isolate will be set to the provided isolate. + * + * Requires there to be no current isolate. Multiple threads may not be in + * the same isolate at once. + */ +DART_EXPORT void Dart_EnterIsolate(Dart_Isolate isolate); + +/** + * Kills the given isolate. + * + * This function has the same effect as dart:isolate's + * Isolate.kill(priority:immediate). + * It can interrupt ordinary Dart code but not native code. If the isolate is + * in the middle of a long running native function, the isolate will not be + * killed until control returns to Dart. + * + * Does not require a current isolate. It is safe to kill the current isolate if + * there is one. + */ +DART_EXPORT void Dart_KillIsolate(Dart_Isolate isolate); + +/** + * Notifies the VM that the embedder expects to be idle until |deadline|. The VM + * may use this time to perform garbage collection or other tasks to avoid + * delays during execution of Dart code in the future. + * + * |deadline| is measured in microseconds against the system's monotonic time. + * This clock can be accessed via Dart_TimelineGetMicros(). + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_NotifyIdle(int64_t deadline); + +typedef void (*Dart_HeapSamplingReportCallback)(void* context, + void* data); + +typedef void* (*Dart_HeapSamplingCreateCallback)( + Dart_Isolate isolate, + Dart_IsolateGroup isolate_group, + const char* cls_name, + intptr_t allocation_size); +typedef void (*Dart_HeapSamplingDeleteCallback)(void* data); + +/** + * Starts the heap sampling profiler for each thread in the VM. + */ +DART_EXPORT void Dart_EnableHeapSampling(void); + +/* + * Stops the heap sampling profiler for each thread in the VM. + */ +DART_EXPORT void Dart_DisableHeapSampling(void); + +/* Registers callbacks are invoked once per sampled allocation upon object + * allocation and garbage collection. + * + * |create_callback| can be used to associate additional data with the sampled + * allocation, such as a stack trace. This data pointer will be passed to + * |delete_callback| to allow for proper disposal when the object associated + * with the allocation sample is collected. + * + * The provided callbacks must not call into the VM and should do as little + * work as possible to avoid performance penalities during object allocation and + * garbage collection. + * + * NOTE: It is a fatal error to set either callback to null once they have been + * initialized. + */ +DART_EXPORT void Dart_RegisterHeapSamplingCallback( + Dart_HeapSamplingCreateCallback create_callback, + Dart_HeapSamplingDeleteCallback delete_callback); + +/* + * Reports the surviving allocation samples for all live isolate groups in the + * VM. + * + * When the callback is invoked: + * - |context| will be the context object provided when invoking + * |Dart_ReportSurvivingAllocations|. This can be safely set to null if not + * required. + * - |heap_size| will be equal to the size of the allocated object associated + * with the sample. + * - |cls_name| will be a C String representing + * the class name of the allocated object. This string is valid for the + * duration of the call to Dart_ReportSurvivingAllocations and can be + * freed by the VM at any point after the method returns. + * - |data| will be set to the data associated with the sample by + * |Dart_HeapSamplingCreateCallback|. + * + * If |force_gc| is true, a full GC will be performed before reporting the + * allocations. + */ +DART_EXPORT void Dart_ReportSurvivingAllocations( + Dart_HeapSamplingReportCallback callback, + void* context, + bool force_gc); + +/* + * Sets the average heap sampling rate based on a number of |bytes| for each + * thread. + * + * In other words, approximately every |bytes| allocated will create a sample. + * Defaults to 512 KiB. + */ +DART_EXPORT void Dart_SetHeapSamplingPeriod(intptr_t bytes); + +/** + * Notifies the VM that the embedder expects the application's working set has + * recently shrunk significantly and is not expected to rise in the near future. + * The VM may spend O(heap-size) time performing clean up work. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_NotifyDestroyed(void); + +/** + * Notifies the VM that the system is running low on memory. + * + * Does not require a current isolate. Only valid after calling Dart_Initialize. + */ +DART_EXPORT void Dart_NotifyLowMemory(void); + +typedef enum { + /** + * Balanced + */ + Dart_PerformanceMode_Default, + /** + * Optimize for low latency, at the expense of throughput and memory overhead + * by performing work in smaller batches (requiring more overhead) or by + * delaying work (requiring more memory). An embedder should not remain in + * this mode indefinitely. + */ + Dart_PerformanceMode_Latency, + /** + * Optimize for high throughput, at the expense of latency and memory overhead + * by performing work in larger batches with more intervening growth. + */ + Dart_PerformanceMode_Throughput, + /** + * Optimize for low memory, at the expensive of throughput and latency by more + * frequently performing work. + */ + Dart_PerformanceMode_Memory, +} Dart_PerformanceMode; + +/** + * Set the desired performance trade-off. + * + * Requires a current isolate. + * + * Returns the previous performance mode. + */ +DART_EXPORT Dart_PerformanceMode +Dart_SetPerformanceMode(Dart_PerformanceMode mode); + +/** + * Starts the CPU sampling profiler. + */ +DART_EXPORT void Dart_StartProfiling(void); + +/** + * Stops the CPU sampling profiler. + * + * Note that some profile samples might still be taken after this function + * returns due to the asynchronous nature of the implementation on some + * platforms. + */ +DART_EXPORT void Dart_StopProfiling(void); + +/** + * Notifies the VM that the current thread should not be profiled until a + * matching call to Dart_ThreadEnableProfiling is made. + * + * NOTE: By default, if a thread has entered an isolate it will be profiled. + * This function should be used when an embedder knows a thread is about + * to make a blocking call and wants to avoid unnecessary interrupts by + * the profiler. + */ +DART_EXPORT void Dart_ThreadDisableProfiling(void); + +/** + * Notifies the VM that the current thread should be profiled. + * + * NOTE: It is only legal to call this function *after* calling + * Dart_ThreadDisableProfiling. + * + * NOTE: By default, if a thread has entered an isolate it will be profiled. + */ +DART_EXPORT void Dart_ThreadEnableProfiling(void); + +/** + * Register symbol information for the Dart VM's profiler and crash dumps. + * + * This consumes the output of //topaz/runtime/dart/profiler_symbols, which + * should be treated as opaque. + */ +DART_EXPORT void Dart_AddSymbols(const char* dso_name, + void* buffer, + intptr_t buffer_size); + +/** + * Exits an isolate. After this call, Dart_CurrentIsolate will + * return NULL. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_ExitIsolate(void); +/* TODO(turnidge): We don't want users of the api to be able to exit a + * "pure" dart isolate. Implement and document. */ + +/** + * Creates a full snapshot of the current isolate heap. + * + * A full snapshot is a compact representation of the dart vm isolate heap + * and dart isolate heap states. These snapshots are used to initialize + * the vm isolate on startup and fast initialization of an isolate. + * A Snapshot of the heap is created before any dart code has executed. + * + * Requires there to be a current isolate. Not available in the precompiled + * runtime (check Dart_IsPrecompiledRuntime). + * + * \param vm_snapshot_data_buffer Returns a pointer to a buffer containing the + * vm snapshot. This buffer is scope allocated and is only valid + * until the next call to Dart_ExitScope. + * \param vm_snapshot_data_size Returns the size of vm_snapshot_data_buffer. + * \param isolate_snapshot_data_buffer Returns a pointer to a buffer containing + * the isolate snapshot. This buffer is scope allocated and is only valid + * until the next call to Dart_ExitScope. + * \param isolate_snapshot_data_size Returns the size of + * isolate_snapshot_data_buffer. + * \param is_core Create a snapshot containing core libraries. + * Such snapshot should be agnostic to null safety mode. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateSnapshot(uint8_t** vm_snapshot_data_buffer, + intptr_t* vm_snapshot_data_size, + uint8_t** isolate_snapshot_data_buffer, + intptr_t* isolate_snapshot_data_size, + bool is_core); + +/** + * Returns whether the buffer contains a kernel file. + * + * \param buffer Pointer to a buffer that might contain a kernel binary. + * \param buffer_size Size of the buffer. + * + * \return Whether the buffer contains a kernel binary (full or partial). + */ +DART_EXPORT bool Dart_IsKernel(const uint8_t* buffer, intptr_t buffer_size); + +/** + * Make isolate runnable. + * + * When isolates are spawned, this function is used to indicate that + * the creation and initialization (including script loading) of the + * isolate is complete and the isolate can start. + * This function expects there to be no current isolate. + * + * \param isolate The isolate to be made runnable. + * + * \return NULL if successful. Returns an error message otherwise. The caller + * is responsible for freeing the error message. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_IsolateMakeRunnable( + Dart_Isolate isolate); + +/* + * ================== + * Messages and Ports + * ================== + */ + +/** + * A port is used to send or receive inter-isolate messages + */ +typedef int64_t Dart_Port; + +/** + * ILLEGAL_PORT is a port number guaranteed never to be associated with a valid + * port. + */ +#define ILLEGAL_PORT ((Dart_Port)0) + +/** + * A message notification callback. + * + * This callback allows the embedder to provide a custom wakeup mechanism for + * the delivery of inter-isolate messages. This function is called once per + * message on an arbitrary thread. It is the responsibility of the embedder to + * eventually call Dart_HandleMessage once per callback received with the + * destination isolate set as the current isolate to process the message. + */ +typedef void (*Dart_MessageNotifyCallback)(Dart_Isolate destination_isolate); + +/** + * Allows embedders to provide a custom wakeup mechanism for the delivery of + * inter-isolate messages. This setting only applies to the current isolate. + * + * This mechanism is optional: if not provided, the isolate will be scheduled on + * a VM-managed thread pool. An embedder should provide this callback if it + * wants to run an isolate on a specific thread or to interleave handling of + * inter-isolate messages with other event sources. + * + * Most embedders will only call this function once, before isolate + * execution begins. If this function is called after isolate + * execution begins, the embedder is responsible for threading issues. + */ +DART_EXPORT void Dart_SetMessageNotifyCallback( + Dart_MessageNotifyCallback message_notify_callback); +/* TODO(turnidge): Consider moving this to isolate creation so that it + * is impossible to mess up. */ + +/** + * Query the current message notify callback for the isolate. + * + * \return The current message notify callback for the isolate. + */ +DART_EXPORT Dart_MessageNotifyCallback Dart_GetMessageNotifyCallback(void); + +/** + * The VM's default message handler supports pausing an isolate before it + * processes the first message and right after the it processes the isolate's + * final message. This can be controlled for all isolates by two VM flags: + * + * `--pause-isolates-on-start` + * `--pause-isolates-on-exit` + * + * Additionally, Dart_SetShouldPauseOnStart and Dart_SetShouldPauseOnExit can be + * used to control this behaviour on a per-isolate basis. + * + * When an embedder is using a Dart_MessageNotifyCallback the embedder + * needs to cooperate with the VM so that the service protocol can report + * accurate information about isolates and so that tools such as debuggers + * work reliably. + * + * The following functions can be used to implement pausing on start and exit. + */ + +/** + * If the VM flag `--pause-isolates-on-start` was passed this will be true. + * + * \return A boolean value indicating if pause on start was requested. + */ +DART_EXPORT bool Dart_ShouldPauseOnStart(void); + +/** + * Override the VM flag `--pause-isolates-on-start` for the current isolate. + * + * \param should_pause Should the isolate be paused on start? + * + * NOTE: This must be called before Dart_IsolateMakeRunnable. + */ +DART_EXPORT void Dart_SetShouldPauseOnStart(bool should_pause); + +/** + * Is the current isolate paused on start? + * + * \return A boolean value indicating if the isolate is paused on start. + */ +DART_EXPORT bool Dart_IsPausedOnStart(void); + +/** + * Called when the embedder has paused the current isolate on start and when + * the embedder has resumed the isolate. + * + * \param paused Is the isolate paused on start? + */ +DART_EXPORT void Dart_SetPausedOnStart(bool paused); + +/** + * If the VM flag `--pause-isolates-on-exit` was passed this will be true. + * + * \return A boolean value indicating if pause on exit was requested. + */ +DART_EXPORT bool Dart_ShouldPauseOnExit(void); + +/** + * Override the VM flag `--pause-isolates-on-exit` for the current isolate. + * + * \param should_pause Should the isolate be paused on exit? + * + */ +DART_EXPORT void Dart_SetShouldPauseOnExit(bool should_pause); + +/** + * Is the current isolate paused on exit? + * + * \return A boolean value indicating if the isolate is paused on exit. + */ +DART_EXPORT bool Dart_IsPausedOnExit(void); + +/** + * Called when the embedder has paused the current isolate on exit and when + * the embedder has resumed the isolate. + * + * \param paused Is the isolate paused on exit? + */ +DART_EXPORT void Dart_SetPausedOnExit(bool paused); + +/** + * Called when the embedder has caught a top level unhandled exception error + * in the current isolate. + * + * NOTE: It is illegal to call this twice on the same isolate without first + * clearing the sticky error to null. + * + * \param error The unhandled exception error. + */ +DART_EXPORT void Dart_SetStickyError(Dart_Handle error); + +/** + * Does the current isolate have a sticky error? + */ +DART_EXPORT bool Dart_HasStickyError(void); + +/** + * Gets the sticky error for the current isolate. + * + * \return A handle to the sticky error object or null. + */ +DART_EXPORT Dart_Handle Dart_GetStickyError(void); + +/** + * Handles the next pending message for the current isolate. + * + * May generate an unhandled exception error. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_HandleMessage(void); + +/** + * Drains the microtask queue, then blocks the calling thread until the current + * isolate receives a message, then handles all messages. + * + * \param timeout_millis When non-zero, the call returns after the indicated + number of milliseconds even if no message was received. + * \return A valid handle if no error occurs, otherwise an error handle. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_WaitForEvent(int64_t timeout_millis); + +/** + * Handles any pending messages for the vm service for the current + * isolate. + * + * This function may be used by an embedder at a breakpoint to avoid + * pausing the vm service. + * + * This function can indirectly cause the message notify callback to + * be called. + * + * \return true if the vm service requests the program resume + * execution, false otherwise + */ +DART_EXPORT bool Dart_HandleServiceMessages(void); + +/** + * Does the current isolate have pending service messages? + * + * \return true if the isolate has pending service messages, false otherwise. + */ +DART_EXPORT bool Dart_HasServiceMessages(void); + +/** + * Processes any incoming messages for the current isolate. + * + * This function may only be used when the embedder has not provided + * an alternate message delivery mechanism with + * Dart_SetMessageCallbacks. It is provided for convenience. + * + * This function waits for incoming messages for the current + * isolate. As new messages arrive, they are handled using + * Dart_HandleMessage. The routine exits when all ports to the + * current isolate are closed. + * + * \return A valid handle if the run loop exited successfully. If an + * exception or other error occurs while processing messages, an + * error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_RunLoop(void); + +/** + * Lets the VM run message processing for the isolate. + * + * This function expects there to a current isolate and the current isolate + * must not have an active api scope. The VM will take care of making the + * isolate runnable (if not already), handles its message loop and will take + * care of shutting the isolate down once it's done. + * + * \param errors_are_fatal Whether uncaught errors should be fatal. + * \param on_error_port A port to notify on uncaught errors (or ILLEGAL_PORT). + * \param on_exit_port A port to notify on exit (or ILLEGAL_PORT). + * \param error A non-NULL pointer which will hold an error message if the call + * fails. The error has to be free()ed by the caller. + * + * \return If successful the VM takes ownership of the isolate and takes care + * of its message loop. If not successful the caller retains ownership of the + * isolate. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT bool Dart_RunLoopAsync( + bool errors_are_fatal, + Dart_Port on_error_port, + Dart_Port on_exit_port, + char** error); + +/* TODO(turnidge): Should this be removed from the public api? */ + +/** + * Gets the main port id for the current isolate. + */ +DART_EXPORT Dart_Port Dart_GetMainPortId(void); + +/** + * Does the current isolate have live ReceivePorts? + * + * A ReceivePort is live when it has not been closed. + */ +DART_EXPORT bool Dart_HasLivePorts(void); + +/** + * Posts a message for some isolate. The message is a serialized + * object. + * + * Requires there to be a current isolate. + * + * For posting messages outside of an isolate see \ref Dart_PostCObject. + * + * \param port_id The destination port. + * \param object An object from the current isolate. + * + * \return True if the message was posted. + */ +DART_EXPORT bool Dart_Post(Dart_Port port_id, Dart_Handle object); + +/** + * Returns a new SendPort with the provided port id. + * + * \param port_id The destination port. + * + * \return A new SendPort if no errors occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewSendPort(Dart_Port port_id); + +/** + * Gets the SendPort id for the provided SendPort. + * \param port A SendPort object whose id is desired. + * \param port_id Returns the id of the SendPort. + * \return Success if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_SendPortGetId(Dart_Handle port, + Dart_Port* port_id); + +/* + * ====== + * Scopes + * ====== + */ + +/** + * Enters a new scope. + * + * All new local handles will be created in this scope. Additionally, + * some functions may return "scope allocated" memory which is only + * valid within this scope. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_EnterScope(void); + +/** + * Exits a scope. + * + * The previous scope (if any) becomes the current scope. + * + * Requires there to be a current isolate. + */ +DART_EXPORT void Dart_ExitScope(void); + +/** + * The Dart VM uses "zone allocation" for temporary structures. Zones + * support very fast allocation of small chunks of memory. The chunks + * cannot be deallocated individually, but instead zones support + * deallocating all chunks in one fast operation. + * + * This function makes it possible for the embedder to allocate + * temporary data in the VMs zone allocator. + * + * Zone allocation is possible: + * 1. when inside a scope where local handles can be allocated + * 2. when processing a message from a native port in a native port + * handler + * + * All the memory allocated this way will be reclaimed either on the + * next call to Dart_ExitScope or when the native port handler exits. + * + * \param size Size of the memory to allocate. + * + * \return A pointer to the allocated memory. NULL if allocation + * failed. Failure might due to is no current VM zone. + */ +DART_EXPORT uint8_t* Dart_ScopeAllocate(intptr_t size); + +/* + * ======= + * Objects + * ======= + */ + +/** + * Returns the null object. + * + * \return A handle to the null object. + */ +DART_EXPORT Dart_Handle Dart_Null(void); + +/** + * Is this object null? + */ +DART_EXPORT bool Dart_IsNull(Dart_Handle object); + +/** + * Returns the empty string object. + * + * \return A handle to the empty string object. + */ +DART_EXPORT Dart_Handle Dart_EmptyString(void); + +/** + * Returns types that are not classes, and which therefore cannot be looked up + * as library members by Dart_GetType. + * + * \return A handle to the dynamic, void or Never type. + */ +DART_EXPORT Dart_Handle Dart_TypeDynamic(void); +DART_EXPORT Dart_Handle Dart_TypeVoid(void); +DART_EXPORT Dart_Handle Dart_TypeNever(void); + +/** + * Checks if the two objects are equal. + * + * The result of the comparison is returned through the 'equal' + * parameter. The return value itself is used to indicate success or + * failure, not equality. + * + * May generate an unhandled exception error. + * + * \param obj1 An object to be compared. + * \param obj2 An object to be compared. + * \param equal Returns the result of the equality comparison. + * + * \return A valid handle if no error occurs during the comparison. + */ +DART_EXPORT Dart_Handle Dart_ObjectEquals(Dart_Handle obj1, + Dart_Handle obj2, + bool* equal); + +/** + * Is this object an instance of some type? + * + * The result of the test is returned through the 'instanceof' parameter. + * The return value itself is used to indicate success or failure. + * + * \param object An object. + * \param type A type. + * \param instanceof Return true if 'object' is an instance of type 'type'. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_ObjectIsType(Dart_Handle object, + Dart_Handle type, + bool* instanceof); + +/** + * Query object type. + * + * \param object Some Object. + * + * \return true if Object is of the specified type. + */ +DART_EXPORT bool Dart_IsInstance(Dart_Handle object); +DART_EXPORT bool Dart_IsNumber(Dart_Handle object); +DART_EXPORT bool Dart_IsInteger(Dart_Handle object); +DART_EXPORT bool Dart_IsDouble(Dart_Handle object); +DART_EXPORT bool Dart_IsBoolean(Dart_Handle object); +DART_EXPORT bool Dart_IsString(Dart_Handle object); +DART_EXPORT bool Dart_IsStringLatin1(Dart_Handle object); /* (ISO-8859-1) */ +DART_EXPORT bool Dart_IsExternalString(Dart_Handle object); +DART_EXPORT bool Dart_IsList(Dart_Handle object); +DART_EXPORT bool Dart_IsMap(Dart_Handle object); +DART_EXPORT bool Dart_IsLibrary(Dart_Handle object); +DART_EXPORT bool Dart_IsType(Dart_Handle handle); +DART_EXPORT bool Dart_IsFunction(Dart_Handle handle); +DART_EXPORT bool Dart_IsVariable(Dart_Handle handle); +DART_EXPORT bool Dart_IsTypeVariable(Dart_Handle handle); +DART_EXPORT bool Dart_IsClosure(Dart_Handle object); +DART_EXPORT bool Dart_IsTypedData(Dart_Handle object); +DART_EXPORT bool Dart_IsByteBuffer(Dart_Handle object); +DART_EXPORT bool Dart_IsFuture(Dart_Handle object); + +/* + * ========= + * Instances + * ========= + */ + +/* + * For the purposes of the embedding api, not all objects returned are + * Dart language objects. Within the api, we use the term 'Instance' + * to indicate handles which refer to true Dart language objects. + * + * TODO(turnidge): Reorganize the "Object" section above, pulling down + * any functions that more properly belong here. */ + +/** + * Gets the type of a Dart language object. + * + * \param instance Some Dart object. + * + * \return If no error occurs, the type is returned. Otherwise an + * error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_InstanceGetType(Dart_Handle instance); + +/** + * Returns the name for the provided class type. + * + * \return A valid string handle if no error occurs during the + * operation. + */ +DART_EXPORT Dart_Handle Dart_ClassName(Dart_Handle cls_type); + +/** + * Returns the name for the provided function or method. + * + * \return A valid string handle if no error occurs during the + * operation. + */ +DART_EXPORT Dart_Handle Dart_FunctionName(Dart_Handle function); + +/** + * Returns a handle to the owner of a function. + * + * The owner of an instance method or a static method is its defining + * class. The owner of a top-level function is its defining + * library. The owner of the function of a non-implicit closure is the + * function of the method or closure that defines the non-implicit + * closure. + * + * \return A valid handle to the owner of the function, or an error + * handle if the argument is not a valid handle to a function. + */ +DART_EXPORT Dart_Handle Dart_FunctionOwner(Dart_Handle function); + +/** + * Determines whether a function handle refers to a static function + * of method. + * + * For the purposes of the embedding API, a top-level function is + * implicitly declared static. + * + * \param function A handle to a function or method declaration. + * \param is_static Returns whether the function or method is declared static. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_FunctionIsStatic(Dart_Handle function, + bool* is_static); + +/** + * Is this object a closure resulting from a tear-off (closurized method)? + * + * Returns true for closures produced when an ordinary method is accessed + * through a getter call. Returns false otherwise, in particular for closures + * produced from local function declarations. + * + * \param object Some Object. + * + * \return true if Object is a tear-off. + */ +DART_EXPORT bool Dart_IsTearOff(Dart_Handle object); + +/** + * Retrieves the function of a closure. + * + * \return A handle to the function of the closure, or an error handle if the + * argument is not a closure. + */ +DART_EXPORT Dart_Handle Dart_ClosureFunction(Dart_Handle closure); + +/** + * Returns a handle to the library which contains class. + * + * \return A valid handle to the library with owns class, null if the class + * has no library or an error handle if the argument is not a valid handle + * to a class type. + */ +DART_EXPORT Dart_Handle Dart_ClassLibrary(Dart_Handle cls_type); + +/* + * ============================= + * Numbers, Integers and Doubles + * ============================= + */ + +/** + * Does this Integer fit into a 64-bit signed integer? + * + * \param integer An integer. + * \param fits Returns true if the integer fits into a 64-bit signed integer. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_IntegerFitsIntoInt64(Dart_Handle integer, + bool* fits); + +/** + * Does this Integer fit into a 64-bit unsigned integer? + * + * \param integer An integer. + * \param fits Returns true if the integer fits into a 64-bit unsigned integer. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_IntegerFitsIntoUint64(Dart_Handle integer, + bool* fits); + +/** + * Returns an Integer with the provided value. + * + * \param value The value of the integer. + * + * \return The Integer object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewInteger(int64_t value); + +/** + * Returns an Integer with the provided value. + * + * \param value The unsigned value of the integer. + * + * \return The Integer object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewIntegerFromUint64(uint64_t value); + +/** + * Returns an Integer with the provided value. + * + * \param value The value of the integer represented as a C string + * containing a hexadecimal number. + * + * \return The Integer object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewIntegerFromHexCString(const char* value); + +/** + * Gets the value of an Integer. + * + * The integer must fit into a 64-bit signed integer, otherwise an error occurs. + * + * \param integer An Integer. + * \param value Returns the value of the Integer. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_IntegerToInt64(Dart_Handle integer, + int64_t* value); + +/** + * Gets the value of an Integer. + * + * The integer must fit into a 64-bit unsigned integer, otherwise an + * error occurs. + * + * \param integer An Integer. + * \param value Returns the value of the Integer. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_IntegerToUint64(Dart_Handle integer, + uint64_t* value); + +/** + * Gets the value of an integer as a hexadecimal C string. + * + * \param integer An Integer. + * \param value Returns the value of the Integer as a hexadecimal C + * string. This C string is scope allocated and is only valid until + * the next call to Dart_ExitScope. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_IntegerToHexCString(Dart_Handle integer, + const char** value); + +/** + * Returns a Double with the provided value. + * + * \param value A double. + * + * \return The Double object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewDouble(double value); + +/** + * Gets the value of a Double + * + * \param double_obj A Double + * \param value Returns the value of the Double. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_DoubleValue(Dart_Handle double_obj, double* value); + +/** + * Returns a closure of static function 'function_name' in the class 'class_name' + * in the exported namespace of specified 'library'. + * + * \param library Library object + * \param cls_type Type object representing a Class + * \param function_name Name of the static function in the class + * + * \return A valid Dart instance if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_GetStaticMethodClosure(Dart_Handle library, + Dart_Handle cls_type, + Dart_Handle function_name); + +/* + * ======== + * Booleans + * ======== + */ + +/** + * Returns the True object. + * + * Requires there to be a current isolate. + * + * \return A handle to the True object. + */ +DART_EXPORT Dart_Handle Dart_True(void); + +/** + * Returns the False object. + * + * Requires there to be a current isolate. + * + * \return A handle to the False object. + */ +DART_EXPORT Dart_Handle Dart_False(void); + +/** + * Returns a Boolean with the provided value. + * + * \param value true or false. + * + * \return The Boolean object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewBoolean(bool value); + +/** + * Gets the value of a Boolean + * + * \param boolean_obj A Boolean + * \param value Returns the value of the Boolean. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_BooleanValue(Dart_Handle boolean_obj, bool* value); + +/* + * ======= + * Strings + * ======= + */ + +/** + * Gets the length of a String. + * + * \param str A String. + * \param length Returns the length of the String. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringLength(Dart_Handle str, intptr_t* length); + +/** + * Returns a String built from the provided C string + * (There is an implicit assumption that the C string passed in contains + * UTF-8 encoded characters and '\0' is considered as a termination + * character). + * + * \param str A C String + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewStringFromCString(const char* str); +/* TODO(turnidge): Document what happens when we run out of memory + * during this call. */ + +/** + * Returns a String built from an array of UTF-8 encoded characters. + * + * \param utf8_array An array of UTF-8 encoded characters. + * \param length The length of the codepoints array. + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewStringFromUTF8(const uint8_t* utf8_array, + intptr_t length); + +/** + * Returns a String built from an array of UTF-16 encoded characters. + * + * \param utf16_array An array of UTF-16 encoded characters. + * \param length The length of the codepoints array. + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewStringFromUTF16(const uint16_t* utf16_array, + intptr_t length); + +/** + * Returns a String built from an array of UTF-32 encoded characters. + * + * \param utf32_array An array of UTF-32 encoded characters. + * \param length The length of the codepoints array. + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewStringFromUTF32(const int32_t* utf32_array, + intptr_t length); + +/** + * Returns a String which references an external array of + * Latin-1 (ISO-8859-1) encoded characters. + * + * \param latin1_array Array of Latin-1 encoded characters. This must not move. + * \param length The length of the characters array. + * \param peer An external pointer to associate with this string. + * \param external_allocation_size The number of externally allocated + * bytes for peer. Used to inform the garbage collector. + * \param callback A callback to be called when this string is finalized. + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle +Dart_NewExternalLatin1String(const uint8_t* latin1_array, + intptr_t length, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); + +/** + * Returns a String which references an external array of UTF-16 encoded + * characters. + * + * \param utf16_array An array of UTF-16 encoded characters. This must not move. + * \param length The length of the characters array. + * \param peer An external pointer to associate with this string. + * \param external_allocation_size The number of externally allocated + * bytes for peer. Used to inform the garbage collector. + * \param callback A callback to be called when this string is finalized. + * + * \return The String object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle +Dart_NewExternalUTF16String(const uint16_t* utf16_array, + intptr_t length, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); + +/** + * Gets the C string representation of a String. + * (It is a sequence of UTF-8 encoded values with a '\0' termination.) + * + * \param str A string. + * \param cstr Returns the String represented as a C string. + * This C string is scope allocated and is only valid until + * the next call to Dart_ExitScope. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringToCString(Dart_Handle str, + const char** cstr); + +/** + * Gets a UTF-8 encoded representation of a String. + * + * Any unpaired surrogate code points in the string will be converted as + * replacement characters (U+FFFD, 0xEF 0xBF 0xBD in UTF-8). If you need + * to preserve unpaired surrogates, use the Dart_StringToUTF16 function. + * + * \param str A string. + * \param utf8_array Returns the String represented as UTF-8 code + * units. This UTF-8 array is scope allocated and is only valid + * until the next call to Dart_ExitScope. + * \param length Used to return the length of the array which was + * actually used. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringToUTF8(Dart_Handle str, + uint8_t** utf8_array, + intptr_t* length); + +/** + * Gets the data corresponding to the string object. This function returns + * the data only for Latin-1 (ISO-8859-1) string objects. For all other + * string objects it returns an error. + * + * \param str A string. + * \param latin1_array An array allocated by the caller, used to return + * the string data. + * \param length Used to pass in the length of the provided array. + * Used to return the length of the array which was actually used. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringToLatin1(Dart_Handle str, + uint8_t* latin1_array, + intptr_t* length); + +/** + * Gets the UTF-16 encoded representation of a string. + * + * \param str A string. + * \param utf16_array An array allocated by the caller, used to return + * the array of UTF-16 encoded characters. + * \param length Used to pass in the length of the provided array. + * Used to return the length of the array which was actually used. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringToUTF16(Dart_Handle str, + uint16_t* utf16_array, + intptr_t* length); + +/** + * Gets the storage size in bytes of a String. + * + * \param str A String. + * \param size Returns the storage size in bytes of the String. + * This is the size in bytes needed to store the String. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_StringStorageSize(Dart_Handle str, intptr_t* size); + +/** + * Retrieves some properties associated with a String. + * Properties retrieved are: + * - character size of the string (one or two byte) + * - length of the string + * - peer pointer of string if it is an external string. + * \param str A String. + * \param char_size Returns the character size of the String. + * \param str_len Returns the length of the String. + * \param peer Returns the peer pointer associated with the String or 0 if + * there is no peer pointer for it. + * \return Success if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_StringGetProperties(Dart_Handle str, + intptr_t* char_size, + intptr_t* str_len, + void** peer); + +/* + * ===== + * Lists + * ===== + */ + +/** + * Returns a List of the desired length. + * + * \param length The length of the list. + * + * \return The List object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewList(intptr_t length); + +typedef enum { + Dart_CoreType_Dynamic, + Dart_CoreType_Int, + Dart_CoreType_String, +} Dart_CoreType_Id; + +// TODO(bkonyi): convert this to use nullable types once NNBD is enabled. +/** + * Returns a List of the desired length with the desired legacy element type. + * + * \param element_type_id The type of elements of the list. + * \param length The length of the list. + * + * \return The List object if no error occurs. Otherwise returns an error + * handle. + */ +DART_EXPORT Dart_Handle Dart_NewListOf(Dart_CoreType_Id element_type_id, + intptr_t length); + +/** + * Returns a List of the desired length with the desired element type. + * + * \param element_type Handle to a nullable type object. E.g., from + * Dart_GetType or Dart_GetNullableType. + * + * \param length The length of the list. + * + * \return The List object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewListOfType(Dart_Handle element_type, + intptr_t length); + +/** + * Returns a List of the desired length with the desired element type, filled + * with the provided object. + * + * \param element_type Handle to a type object. E.g., from Dart_GetType. + * + * \param fill_object Handle to an object of type 'element_type' that will be + * used to populate the list. This parameter can only be Dart_Null() if the + * length of the list is 0 or 'element_type' is a nullable type. + * + * \param length The length of the list. + * + * \return The List object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewListOfTypeFilled(Dart_Handle element_type, + Dart_Handle fill_object, + intptr_t length); + +/** + * Gets the length of a List. + * + * May generate an unhandled exception error. + * + * \param list A List. + * \param length Returns the length of the List. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_ListLength(Dart_Handle list, intptr_t* length); + +/** + * Gets the Object at some index of a List. + * + * If the index is out of bounds, an error occurs. + * + * May generate an unhandled exception error. + * + * \param list A List. + * \param index A valid index into the List. + * + * \return The Object in the List at the specified index if no error + * occurs. Otherwise returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_ListGetAt(Dart_Handle list, intptr_t index); + +/** +* Gets a range of Objects from a List. +* +* If any of the requested index values are out of bounds, an error occurs. +* +* May generate an unhandled exception error. +* +* \param list A List. +* \param offset The offset of the first item to get. +* \param length The number of items to get. +* \param result A pointer to fill with the objects. +* +* \return Success if no error occurs during the operation. +*/ +DART_EXPORT Dart_Handle Dart_ListGetRange(Dart_Handle list, + intptr_t offset, + intptr_t length, + Dart_Handle* result); + +/** + * Sets the Object at some index of a List. + * + * If the index is out of bounds, an error occurs. + * + * May generate an unhandled exception error. + * + * \param list A List. + * \param index A valid index into the List. + * \param value The Object to put in the List. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT Dart_Handle Dart_ListSetAt(Dart_Handle list, + intptr_t index, + Dart_Handle value); + +/** + * May generate an unhandled exception error. + */ +DART_EXPORT Dart_Handle Dart_ListGetAsBytes(Dart_Handle list, + intptr_t offset, + uint8_t* native_array, + intptr_t length); + +/** + * May generate an unhandled exception error. + */ +DART_EXPORT Dart_Handle Dart_ListSetAsBytes(Dart_Handle list, + intptr_t offset, + const uint8_t* native_array, + intptr_t length); + +/* + * ==== + * Maps + * ==== + */ + +/** + * Gets the Object at some key of a Map. + * + * May generate an unhandled exception error. + * + * \param map A Map. + * \param key An Object. + * + * \return The value in the map at the specified key, null if the map does not + * contain the key, or an error handle. + */ +DART_EXPORT Dart_Handle Dart_MapGetAt(Dart_Handle map, Dart_Handle key); + +/** + * Returns whether the Map contains a given key. + * + * May generate an unhandled exception error. + * + * \param map A Map. + * + * \return A handle on a boolean indicating whether map contains the key. + * Otherwise returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_MapContainsKey(Dart_Handle map, Dart_Handle key); + +/** + * Gets the list of keys of a Map. + * + * May generate an unhandled exception error. + * + * \param map A Map. + * + * \return The list of key Objects if no error occurs. Otherwise returns an + * error handle. + */ +DART_EXPORT Dart_Handle Dart_MapKeys(Dart_Handle map); + +/* + * ========== + * Typed Data + * ========== + */ + +typedef enum { + Dart_TypedData_kByteData = 0, + Dart_TypedData_kInt8, + Dart_TypedData_kUint8, + Dart_TypedData_kUint8Clamped, + Dart_TypedData_kInt16, + Dart_TypedData_kUint16, + Dart_TypedData_kInt32, + Dart_TypedData_kUint32, + Dart_TypedData_kInt64, + Dart_TypedData_kUint64, + Dart_TypedData_kFloat32, + Dart_TypedData_kFloat64, + Dart_TypedData_kInt32x4, + Dart_TypedData_kFloat32x4, + Dart_TypedData_kFloat64x2, + Dart_TypedData_kInvalid +} Dart_TypedData_Type; + +/** + * Return type if this object is a TypedData object. + * + * \return kInvalid if the object is not a TypedData object or the appropriate + * Dart_TypedData_Type. + */ +DART_EXPORT Dart_TypedData_Type Dart_GetTypeOfTypedData(Dart_Handle object); + +/** + * Return type if this object is an external TypedData object. + * + * \return kInvalid if the object is not an external TypedData object or + * the appropriate Dart_TypedData_Type. + */ +DART_EXPORT Dart_TypedData_Type +Dart_GetTypeOfExternalTypedData(Dart_Handle object); + +/** + * Returns a TypedData object of the desired length and type. + * + * \param type The type of the TypedData object. + * \param length The length of the TypedData object (length in type units). + * + * \return The TypedData object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewTypedData(Dart_TypedData_Type type, + intptr_t length); + +/** + * Returns a TypedData object which references an external data array. + * + * \param type The type of the data array. + * \param data A data array. This array must not move. + * \param length The length of the data array (length in type units). + * + * \return The TypedData object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewExternalTypedData(Dart_TypedData_Type type, + void* data, + intptr_t length); + +/** + * Returns a TypedData object which references an external data array. + * + * \param type The type of the data array. + * \param data A data array. This array must not move. + * \param length The length of the data array (length in type units). + * \param peer A pointer to a native object or NULL. This value is + * provided to callback when it is invoked. + * \param external_allocation_size The number of externally allocated + * bytes for peer. Used to inform the garbage collector. + * \param callback A function pointer that will be invoked sometime + * after the object is garbage collected, unless the handle has been deleted. + * A valid callback needs to be specified it cannot be NULL. + * + * \return The TypedData object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle +Dart_NewExternalTypedDataWithFinalizer(Dart_TypedData_Type type, + void* data, + intptr_t length, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); +DART_EXPORT Dart_Handle Dart_NewUnmodifiableExternalTypedDataWithFinalizer( + Dart_TypedData_Type type, + const void* data, + intptr_t length, + void* peer, + intptr_t external_allocation_size, + Dart_HandleFinalizer callback); + +/** + * Returns a ByteBuffer object for the typed data. + * + * \param typed_data The TypedData object. + * + * \return The ByteBuffer object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewByteBuffer(Dart_Handle typed_data); + +/** + * Acquires access to the internal data address of a TypedData object. + * + * \param object The typed data object whose internal data address is to + * be accessed. + * \param type The type of the object is returned here. + * \param data The internal data address is returned here. + * \param len Size of the typed array is returned here. + * + * Notes: + * When the internal address of the object is acquired any calls to a + * Dart API function that could potentially allocate an object or run + * any Dart code will return an error. + * + * Any Dart API functions for accessing the data should not be called + * before the corresponding release. In particular, the object should + * not be acquired again before its release. This leads to undefined + * behavior. + * + * \return Success if the internal data address is acquired successfully. + * Otherwise, returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_TypedDataAcquireData(Dart_Handle object, + Dart_TypedData_Type* type, + void** data, + intptr_t* len); + +/** + * Releases access to the internal data address that was acquired earlier using + * Dart_TypedDataAcquireData. + * + * \param object The typed data object whose internal data address is to be + * released. + * + * \return Success if the internal data address is released successfully. + * Otherwise, returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_TypedDataReleaseData(Dart_Handle object); + +/** + * Returns the TypedData object associated with the ByteBuffer object. + * + * \param byte_buffer The ByteBuffer object. + * + * \return The TypedData object if no error occurs. Otherwise returns + * an error handle. + */ +DART_EXPORT Dart_Handle Dart_GetDataFromByteBuffer(Dart_Handle byte_buffer); + +/* + * ============================================================ + * Invoking Constructors, Methods, Closures and Field accessors + * ============================================================ + */ + +/** + * Invokes a constructor, creating a new object. + * + * This function allows hidden constructors (constructors with leading + * underscores) to be called. + * + * \param type Type of object to be constructed. + * \param constructor_name The name of the constructor to invoke. Use + * Dart_Null() or Dart_EmptyString() to invoke the unnamed constructor. + * This name should not include the name of the class. + * \param number_of_arguments Size of the arguments array. + * \param arguments An array of arguments to the constructor. + * + * \return If the constructor is called and completes successfully, + * then the new object. If an error occurs during execution, then an + * error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_New(Dart_Handle type, + Dart_Handle constructor_name, + int number_of_arguments, + Dart_Handle* arguments); + +/** + * Allocate a new object without invoking a constructor. + * + * \param type The type of an object to be allocated. + * + * \return The new object. If an error occurs during execution, then an + * error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_Allocate(Dart_Handle type); + +/** + * Allocate a new object without invoking a constructor, and sets specified + * native fields. + * + * \param type The type of an object to be allocated. + * \param num_native_fields The number of native fields to set. + * \param native_fields An array containing the value of native fields. + * + * \return The new object. If an error occurs during execution, then an + * error handle is returned. + */ +DART_EXPORT Dart_Handle +Dart_AllocateWithNativeFields(Dart_Handle type, + intptr_t num_native_fields, + const intptr_t* native_fields); + +/** + * Invokes a method or function. + * + * The 'target' parameter may be an object, type, or library. If + * 'target' is an object, then this function will invoke an instance + * method. If 'target' is a type, then this function will invoke a + * static method. If 'target' is a library, then this function will + * invoke a top-level function from that library. + * NOTE: This API call cannot be used to invoke methods of a type object. + * + * This function ignores visibility (leading underscores in names). + * + * May generate an unhandled exception error. + * + * \param target An object, type, or library. + * \param name The name of the function or method to invoke. + * \param number_of_arguments Size of the arguments array. + * \param arguments An array of arguments to the function. + * + * \return If the function or method is called and completes + * successfully, then the return value is returned. If an error + * occurs during execution, then an error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_Invoke(Dart_Handle target, + Dart_Handle name, + int number_of_arguments, + Dart_Handle* arguments); +/* TODO(turnidge): Document how to invoke operators. */ + +/** + * Invokes a Closure with the given arguments. + * + * May generate an unhandled exception error. + * + * \return If no error occurs during execution, then the result of + * invoking the closure is returned. If an error occurs during + * execution, then an error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_InvokeClosure(Dart_Handle closure, + int number_of_arguments, + Dart_Handle* arguments); + +/** + * Invokes a Generative Constructor on an object that was previously + * allocated using Dart_Allocate/Dart_AllocateWithNativeFields. + * + * The 'object' parameter must be an object. + * + * This function ignores visibility (leading underscores in names). + * + * May generate an unhandled exception error. + * + * \param object An object. + * \param name The name of the constructor to invoke. + * Use Dart_Null() or Dart_EmptyString() to invoke the unnamed constructor. + * \param number_of_arguments Size of the arguments array. + * \param arguments An array of arguments to the function. + * + * \return If the constructor is called and completes + * successfully, then the object is returned. If an error + * occurs during execution, then an error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_InvokeConstructor(Dart_Handle object, + Dart_Handle name, + int number_of_arguments, + Dart_Handle* arguments); + +/** + * Gets the value of a field. + * + * The 'container' parameter may be an object, type, or library. If + * 'container' is an object, then this function will access an + * instance field. If 'container' is a type, then this function will + * access a static field. If 'container' is a library, then this + * function will access a top-level variable. + * NOTE: This API call cannot be used to access fields of a type object. + * + * This function ignores field visibility (leading underscores in names). + * + * May generate an unhandled exception error. + * + * \param container An object, type, or library. + * \param name A field name. + * + * \return If no error occurs, then the value of the field is + * returned. Otherwise an error handle is returned. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_GetField(Dart_Handle container, Dart_Handle name); + +/** + * Sets the value of a field. + * + * The 'container' parameter may actually be an object, type, or + * library. If 'container' is an object, then this function will + * access an instance field. If 'container' is a type, then this + * function will access a static field. If 'container' is a library, + * then this function will access a top-level variable. + * NOTE: This API call cannot be used to access fields of a type object. + * + * This function ignores field visibility (leading underscores in names). + * + * May generate an unhandled exception error. + * + * \param container An object, type, or library. + * \param name A field name. + * \param value The new field value. + * + * \return A valid handle if no error occurs. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_SetField(Dart_Handle container, Dart_Handle name, Dart_Handle value); + +/* + * ========== + * Exceptions + * ========== + */ + +/* + * TODO(turnidge): Remove these functions from the api and replace all + * uses with Dart_NewUnhandledExceptionError. */ + +/** + * Throws an exception. + * + * This function causes a Dart language exception to be thrown. This + * will proceed in the standard way, walking up Dart frames until an + * appropriate 'catch' block is found, executing 'finally' blocks, + * etc. + * + * If an error handle is passed into this function, the error is + * propagated immediately. See Dart_PropagateError for a discussion + * of error propagation. + * + * If successful, this function does not return. Note that this means + * that the destructors of any stack-allocated C++ objects will not be + * called. If there are no Dart frames on the stack, an error occurs. + * + * \return An error handle if the exception was not thrown. + * Otherwise the function does not return. + */ +DART_EXPORT Dart_Handle Dart_ThrowException(Dart_Handle exception); + +/** + * Rethrows an exception. + * + * Rethrows an exception, unwinding all dart frames on the stack. If + * successful, this function does not return. Note that this means + * that the destructors of any stack-allocated C++ objects will not be + * called. If there are no Dart frames on the stack, an error occurs. + * + * \return An error handle if the exception was not thrown. + * Otherwise the function does not return. + */ +DART_EXPORT Dart_Handle Dart_ReThrowException(Dart_Handle exception, + Dart_Handle stacktrace); + +/* + * =========================== + * Native fields and functions + * =========================== + */ + +/** + * Gets the number of native instance fields in an object. + */ +DART_EXPORT Dart_Handle Dart_GetNativeInstanceFieldCount(Dart_Handle obj, + int* count); + +/** + * Gets the value of a native field. + * + * TODO(turnidge): Document. + */ +DART_EXPORT Dart_Handle Dart_GetNativeInstanceField(Dart_Handle obj, + int index, + intptr_t* value); + +/** + * Sets the value of a native field. + * + * TODO(turnidge): Document. + */ +DART_EXPORT Dart_Handle Dart_SetNativeInstanceField(Dart_Handle obj, + int index, + intptr_t value); + +/** + * The arguments to a native function. + * + * This object is passed to a native function to represent its + * arguments and return value. It allows access to the arguments to a + * native function by index. It also allows the return value of a + * native function to be set. + */ +typedef struct _Dart_NativeArguments* Dart_NativeArguments; + +/** + * Extracts current isolate group data from the native arguments structure. + */ +DART_EXPORT void* Dart_GetNativeIsolateGroupData(Dart_NativeArguments args); + +typedef enum { + Dart_NativeArgument_kBool = 0, + Dart_NativeArgument_kInt32, + Dart_NativeArgument_kUint32, + Dart_NativeArgument_kInt64, + Dart_NativeArgument_kUint64, + Dart_NativeArgument_kDouble, + Dart_NativeArgument_kString, + Dart_NativeArgument_kInstance, + Dart_NativeArgument_kNativeFields, +} Dart_NativeArgument_Type; + +typedef struct _Dart_NativeArgument_Descriptor { + uint8_t type; + uint8_t index; +} Dart_NativeArgument_Descriptor; + +typedef union _Dart_NativeArgument_Value { + bool as_bool; + int32_t as_int32; + uint32_t as_uint32; + int64_t as_int64; + uint64_t as_uint64; + double as_double; + struct { + Dart_Handle dart_str; + void* peer; + } as_string; + struct { + intptr_t num_fields; + intptr_t* values; + } as_native_fields; + Dart_Handle as_instance; +} Dart_NativeArgument_Value; + +enum { + kNativeArgNumberPos = 0, + kNativeArgNumberSize = 8, + kNativeArgTypePos = kNativeArgNumberPos + kNativeArgNumberSize, + kNativeArgTypeSize = 8, +}; + +#define BITMASK(size) ((1 << size) - 1) +#define DART_NATIVE_ARG_DESCRIPTOR(type, position) \ + (((type & BITMASK(kNativeArgTypeSize)) << kNativeArgTypePos) | \ + (position & BITMASK(kNativeArgNumberSize))) + +/** + * Gets the native arguments based on the types passed in and populates + * the passed arguments buffer with appropriate native values. + * + * \param args the Native arguments block passed into the native call. + * \param num_arguments length of argument descriptor array and argument + * values array passed in. + * \param arg_descriptors an array that describes the arguments that + * need to be retrieved. For each argument to be retrieved the descriptor + * contains the argument number (0, 1 etc.) and the argument type + * described using Dart_NativeArgument_Type, e.g: + * DART_NATIVE_ARG_DESCRIPTOR(Dart_NativeArgument_kBool, 1) indicates + * that the first argument is to be retrieved and it should be a boolean. + * \param arg_values array into which the native arguments need to be + * extracted into, the array is allocated by the caller (it could be + * stack allocated to avoid the malloc/free performance overhead). + * + * \return Success if all the arguments could be extracted correctly, + * returns an error handle if there were any errors while extracting the + * arguments (mismatched number of arguments, incorrect types, etc.). + */ +DART_EXPORT Dart_Handle +Dart_GetNativeArguments(Dart_NativeArguments args, + int num_arguments, + const Dart_NativeArgument_Descriptor* arg_descriptors, + Dart_NativeArgument_Value* arg_values); + +/** + * Gets the native argument at some index. + */ +DART_EXPORT Dart_Handle Dart_GetNativeArgument(Dart_NativeArguments args, + int index); +/* TODO(turnidge): Specify the behavior of an out-of-bounds access. */ + +/** + * Gets the number of native arguments. + */ +DART_EXPORT int Dart_GetNativeArgumentCount(Dart_NativeArguments args); + +/** + * Gets all the native fields of the native argument at some index. + * \param args Native arguments structure. + * \param arg_index Index of the desired argument in the structure above. + * \param num_fields size of the intptr_t array 'field_values' passed in. + * \param field_values intptr_t array in which native field values are returned. + * \return Success if the native fields where copied in successfully. Otherwise + * returns an error handle. On success the native field values are copied + * into the 'field_values' array, if the argument at 'arg_index' is a + * null object then 0 is copied as the native field values into the + * 'field_values' array. + */ +DART_EXPORT Dart_Handle +Dart_GetNativeFieldsOfArgument(Dart_NativeArguments args, + int arg_index, + int num_fields, + intptr_t* field_values); + +/** + * Gets the native field of the receiver. + */ +DART_EXPORT Dart_Handle Dart_GetNativeReceiver(Dart_NativeArguments args, + intptr_t* value); + +/** + * Gets a string native argument at some index. + * \param args Native arguments structure. + * \param arg_index Index of the desired argument in the structure above. + * \param peer Returns the peer pointer if the string argument has one. + * \return Success if the string argument has a peer, if it does not + * have a peer then the String object is returned. Otherwise returns + * an error handle (argument is not a String object). + */ +DART_EXPORT Dart_Handle Dart_GetNativeStringArgument(Dart_NativeArguments args, + int arg_index, + void** peer); + +/** + * Gets an integer native argument at some index. + * \param args Native arguments structure. + * \param index Index of the desired argument in the structure above. + * \param value Returns the integer value if the argument is an Integer. + * \return Success if no error occurs. Otherwise returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_GetNativeIntegerArgument(Dart_NativeArguments args, + int index, + int64_t* value); + +/** + * Gets a boolean native argument at some index. + * \param args Native arguments structure. + * \param index Index of the desired argument in the structure above. + * \param value Returns the boolean value if the argument is a Boolean. + * \return Success if no error occurs. Otherwise returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_GetNativeBooleanArgument(Dart_NativeArguments args, + int index, + bool* value); + +/** + * Gets a double native argument at some index. + * \param args Native arguments structure. + * \param index Index of the desired argument in the structure above. + * \param value Returns the double value if the argument is a double. + * \return Success if no error occurs. Otherwise returns an error handle. + */ +DART_EXPORT Dart_Handle Dart_GetNativeDoubleArgument(Dart_NativeArguments args, + int index, + double* value); + +/** + * Sets the return value for a native function. + * + * If retval is an Error handle, then error will be propagated once + * the native functions exits. See Dart_PropagateError for a + * discussion of how different types of errors are propagated. + */ +DART_EXPORT void Dart_SetReturnValue(Dart_NativeArguments args, + Dart_Handle retval); + +DART_EXPORT void Dart_SetWeakHandleReturnValue(Dart_NativeArguments args, + Dart_WeakPersistentHandle rval); + +DART_EXPORT void Dart_SetBooleanReturnValue(Dart_NativeArguments args, + bool retval); + +DART_EXPORT void Dart_SetIntegerReturnValue(Dart_NativeArguments args, + int64_t retval); + +DART_EXPORT void Dart_SetDoubleReturnValue(Dart_NativeArguments args, + double retval); + +/** + * A native function. + */ +typedef void (*Dart_NativeFunction)(Dart_NativeArguments arguments); + +/** + * Native entry resolution callback. + * + * For libraries and scripts which have native functions, the embedder + * can provide a native entry resolver. This callback is used to map a + * name/arity to a Dart_NativeFunction. If no function is found, the + * callback should return NULL. + * + * The parameters to the native resolver function are: + * \param name a Dart string which is the name of the native function. + * \param num_of_arguments is the number of arguments expected by the + * native function. + * \param auto_setup_scope is a boolean flag that can be set by the resolver + * to indicate if this function needs a Dart API scope (see Dart_EnterScope/ + * Dart_ExitScope) to be setup automatically by the VM before calling into + * the native function. By default most native functions would require this + * to be true but some light weight native functions which do not call back + * into the VM through the Dart API may not require a Dart scope to be + * setup automatically. + * + * \return A valid Dart_NativeFunction which resolves to a native entry point + * for the native function. + * + * See Dart_SetNativeResolver. + */ +typedef Dart_NativeFunction (*Dart_NativeEntryResolver)(Dart_Handle name, + int num_of_arguments, + bool* auto_setup_scope); +/* TODO(turnidge): Consider renaming to NativeFunctionResolver or + * NativeResolver. */ + +/** + * Native entry symbol lookup callback. + * + * For libraries and scripts which have native functions, the embedder + * can provide a callback for mapping a native entry to a symbol. This callback + * maps a native function entry PC to the native function name. If no native + * entry symbol can be found, the callback should return NULL. + * + * The parameters to the native reverse resolver function are: + * \param nf A Dart_NativeFunction. + * + * \return A const UTF-8 string containing the symbol name or NULL. + * + * See Dart_SetNativeResolver. + */ +typedef const uint8_t* (*Dart_NativeEntrySymbol)(Dart_NativeFunction nf); + +/** + * FFI Native C function pointer resolver callback. + * + * See Dart_SetFfiNativeResolver. + */ +typedef void* (*Dart_FfiNativeResolver)(const char* name, uintptr_t args_n); + +/* + * =========== + * Environment + * =========== + */ + +/** + * An environment lookup callback function. + * + * \param name The name of the value to lookup in the environment. + * + * \return A valid handle to a string if the name exists in the + * current environment or Dart_Null() if not. + */ +typedef Dart_Handle (*Dart_EnvironmentCallback)(Dart_Handle name); + +/** + * Sets the environment callback for the current isolate. This + * callback is used to lookup environment values by name in the + * current environment. This enables the embedder to supply values for + * the const constructors bool.fromEnvironment, int.fromEnvironment + * and String.fromEnvironment. + */ +DART_EXPORT Dart_Handle +Dart_SetEnvironmentCallback(Dart_EnvironmentCallback callback); + +/** + * Sets the callback used to resolve native functions for a library. + * + * \param library A library. + * \param resolver A native entry resolver. + * + * \return A valid handle if the native resolver was set successfully. + */ +DART_EXPORT Dart_Handle +Dart_SetNativeResolver(Dart_Handle library, + Dart_NativeEntryResolver resolver, + Dart_NativeEntrySymbol symbol); +/* TODO(turnidge): Rename to Dart_LibrarySetNativeResolver? */ + +/** + * Returns the callback used to resolve native functions for a library. + * + * \param library A library. + * \param resolver a pointer to a Dart_NativeEntryResolver + * + * \return A valid handle if the library was found. + */ +DART_EXPORT Dart_Handle +Dart_GetNativeResolver(Dart_Handle library, Dart_NativeEntryResolver* resolver); + +/** + * Returns the callback used to resolve native function symbols for a library. + * + * \param library A library. + * \param resolver a pointer to a Dart_NativeEntrySymbol. + * + * \return A valid handle if the library was found. + */ +DART_EXPORT Dart_Handle Dart_GetNativeSymbol(Dart_Handle library, + Dart_NativeEntrySymbol* resolver); + +/** + * Sets the callback used to resolve FFI native functions for a library. + * The resolved functions are expected to be a C function pointer of the + * correct signature (as specified in the `@FfiNative()` function + * annotation in Dart code). + * + * NOTE: This is an experimental feature and might change in the future. + * + * \param library A library. + * \param resolver A native function resolver. + * + * \return A valid handle if the native resolver was set successfully. + */ +DART_EXPORT Dart_Handle +Dart_SetFfiNativeResolver(Dart_Handle library, Dart_FfiNativeResolver resolver); + +/* + * ===================== + * Scripts and Libraries + * ===================== + */ + +typedef enum { + Dart_kCanonicalizeUrl = 0, + Dart_kImportTag, + Dart_kKernelTag, +} Dart_LibraryTag; + +/** + * The library tag handler is a multi-purpose callback provided by the + * embedder to the Dart VM. The embedder implements the tag handler to + * provide the ability to load Dart scripts and imports. + * + * -- TAGS -- + * + * Dart_kCanonicalizeUrl + * + * This tag indicates that the embedder should canonicalize 'url' with + * respect to 'library'. For most embedders, the + * Dart_DefaultCanonicalizeUrl function is a sufficient implementation + * of this tag. The return value should be a string holding the + * canonicalized url. + * + * Dart_kImportTag + * + * This tag is used to load a library from IsolateMirror.loadUri. The embedder + * should call Dart_LoadLibraryFromKernel to provide the library to the VM. The + * return value should be an error or library (the result from + * Dart_LoadLibraryFromKernel). + * + * Dart_kKernelTag + * + * This tag is used to load the intermediate file (kernel) generated by + * the Dart front end. This tag is typically used when a 'hot-reload' + * of an application is needed and the VM is 'use dart front end' mode. + * The dart front end typically compiles all the scripts, imports and part + * files into one intermediate file hence we don't use the source/import or + * script tags. The return value should be an error or a TypedData containing + * the kernel bytes. + * + */ +typedef Dart_Handle (*Dart_LibraryTagHandler)( + Dart_LibraryTag tag, + Dart_Handle library_or_package_map_url, + Dart_Handle url); + +/** + * Sets library tag handler for the current isolate. This handler is + * used to handle the various tags encountered while loading libraries + * or scripts in the isolate. + * + * \param handler Handler code to be used for handling the various tags + * encountered while loading libraries or scripts in the isolate. + * + * \return If no error occurs, the handler is set for the isolate. + * Otherwise an error handle is returned. + * + * TODO(turnidge): Document. + */ +DART_EXPORT Dart_Handle +Dart_SetLibraryTagHandler(Dart_LibraryTagHandler handler); + +/** + * Handles deferred loading requests. When this handler is invoked, it should + * eventually load the deferred loading unit with the given id and call + * Dart_DeferredLoadComplete or Dart_DeferredLoadCompleteError. It is + * recommended that the loading occur asynchronously, but it is permitted to + * call Dart_DeferredLoadComplete or Dart_DeferredLoadCompleteError before the + * handler returns. + * + * If an error is returned, it will be propagated through + * `prefix.loadLibrary()`. This is useful for synchronous + * implementations, which must propagate any unwind errors from + * Dart_DeferredLoadComplete or Dart_DeferredLoadComplete. Otherwise the handler + * should return a non-error such as `Dart_Null()`. + */ +typedef Dart_Handle (*Dart_DeferredLoadHandler)(intptr_t loading_unit_id); + +/** + * Sets the deferred load handler for the current isolate. This handler is + * used to handle loading deferred imports in an AppJIT or AppAOT program. + */ +DART_EXPORT Dart_Handle +Dart_SetDeferredLoadHandler(Dart_DeferredLoadHandler handler); + +/** + * Notifies the VM that a deferred load completed successfully. This function + * will eventually cause the corresponding `prefix.loadLibrary()` futures to + * complete. + * + * Requires the current isolate to be the same current isolate during the + * invocation of the Dart_DeferredLoadHandler. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_DeferredLoadComplete(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions); + +/** + * Notifies the VM that a deferred load failed. This function + * will eventually cause the corresponding `prefix.loadLibrary()` futures to + * complete with an error. + * + * If `transient` is true, future invocations of `prefix.loadLibrary()` will + * trigger new load requests. If false, futures invocation will complete with + * the same error. + * + * Requires the current isolate to be the same current isolate during the + * invocation of the Dart_DeferredLoadHandler. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_DeferredLoadCompleteError(intptr_t loading_unit_id, + const char* error_message, + bool transient); + +/** + * Canonicalizes a url with respect to some library. + * + * The url is resolved with respect to the library's url and some url + * normalizations are performed. + * + * This canonicalization function should be sufficient for most + * embedders to implement the Dart_kCanonicalizeUrl tag. + * + * \param base_url The base url relative to which the url is + * being resolved. + * \param url The url being resolved and canonicalized. This + * parameter is a string handle. + * + * \return If no error occurs, a String object is returned. Otherwise + * an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_DefaultCanonicalizeUrl(Dart_Handle base_url, + Dart_Handle url); + +/** + * Loads the root library for the current isolate. + * + * Requires there to be no current root library. + * + * \param kernel_buffer A buffer which contains a kernel binary (see + * pkg/kernel/binary.md). Must remain valid until isolate group shutdown. + * \param kernel_size Length of the passed in buffer. + * + * \return A handle to the root library, or an error. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_LoadScriptFromKernel(const uint8_t* kernel_buffer, intptr_t kernel_size); + +/** + * Gets the library for the root script for the current isolate. + * + * If the root script has not yet been set for the current isolate, + * this function returns Dart_Null(). This function never returns an + * error handle. + * + * \return Returns the root Library for the current isolate or Dart_Null(). + */ +DART_EXPORT Dart_Handle Dart_RootLibrary(void); + +/** + * Sets the root library for the current isolate. + * + * \return Returns an error handle if `library` is not a library handle. + */ +DART_EXPORT Dart_Handle Dart_SetRootLibrary(Dart_Handle library); + +/** + * Lookup or instantiate a legacy type by name and type arguments from a + * Library. + * + * \param library The library containing the class or interface. + * \param class_name The class name for the type. + * \param number_of_type_arguments Number of type arguments. + * For non parametric types the number of type arguments would be 0. + * \param type_arguments Pointer to an array of type arguments. + * For non parametric types a NULL would be passed in for this argument. + * + * \return If no error occurs, the type is returned. + * Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_GetType(Dart_Handle library, + Dart_Handle class_name, + intptr_t number_of_type_arguments, + Dart_Handle* type_arguments); + +/** + * Lookup or instantiate a nullable type by name and type arguments from + * Library. + * + * \param library The library containing the class or interface. + * \param class_name The class name for the type. + * \param number_of_type_arguments Number of type arguments. + * For non parametric types the number of type arguments would be 0. + * \param type_arguments Pointer to an array of type arguments. + * For non parametric types a NULL would be passed in for this argument. + * + * \return If no error occurs, the type is returned. + * Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_GetNullableType(Dart_Handle library, + Dart_Handle class_name, + intptr_t number_of_type_arguments, + Dart_Handle* type_arguments); + +/** + * Lookup or instantiate a non-nullable type by name and type arguments from + * Library. + * + * \param library The library containing the class or interface. + * \param class_name The class name for the type. + * \param number_of_type_arguments Number of type arguments. + * For non parametric types the number of type arguments would be 0. + * \param type_arguments Pointer to an array of type arguments. + * For non parametric types a NULL would be passed in for this argument. + * + * \return If no error occurs, the type is returned. + * Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle +Dart_GetNonNullableType(Dart_Handle library, + Dart_Handle class_name, + intptr_t number_of_type_arguments, + Dart_Handle* type_arguments); + +/** + * Creates a nullable version of the provided type. + * + * \param type The type to be converted to a nullable type. + * + * \return If no error occurs, a nullable type is returned. + * Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_TypeToNullableType(Dart_Handle type); + +/** + * Creates a non-nullable version of the provided type. + * + * \param type The type to be converted to a non-nullable type. + * + * \return If no error occurs, a non-nullable type is returned. + * Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_TypeToNonNullableType(Dart_Handle type); + +/** + * A type's nullability. + * + * \param type A Dart type. + * \param result An out parameter containing the result of the check. True if + * the type is of the specified nullability, false otherwise. + * + * \return Returns an error handle if type is not of type Type. + */ +DART_EXPORT Dart_Handle Dart_IsNullableType(Dart_Handle type, bool* result); +DART_EXPORT Dart_Handle Dart_IsNonNullableType(Dart_Handle type, bool* result); +DART_EXPORT Dart_Handle Dart_IsLegacyType(Dart_Handle type, bool* result); + +/** + * Lookup a class or interface by name from a Library. + * + * \param library The library containing the class or interface. + * \param class_name The name of the class or interface. + * + * \return If no error occurs, the class or interface is + * returned. Otherwise an error handle is returned. + */ +DART_EXPORT Dart_Handle Dart_GetClass(Dart_Handle library, + Dart_Handle class_name); +/* TODO(asiva): The above method needs to be removed once all uses + * of it are removed from the embedder code. */ + +/** + * Returns an import path to a Library, such as "file:///test.dart" or + * "dart:core". + */ +DART_EXPORT Dart_Handle Dart_LibraryUrl(Dart_Handle library); + +/** + * Returns a URL from which a Library was loaded. + */ +DART_EXPORT Dart_Handle Dart_LibraryResolvedUrl(Dart_Handle library); + +/** + * \return An array of libraries. + */ +DART_EXPORT Dart_Handle Dart_GetLoadedLibraries(void); + +DART_EXPORT Dart_Handle Dart_LookupLibrary(Dart_Handle url); +/* TODO(turnidge): Consider returning Dart_Null() when the library is + * not found to distinguish that from a true error case. */ + +/** + * Report an loading error for the library. + * + * \param library The library that failed to load. + * \param error The Dart error instance containing the load error. + * + * \return If the VM handles the error, the return value is + * a null handle. If it doesn't handle the error, the error + * object is returned. + */ +DART_EXPORT Dart_Handle Dart_LibraryHandleError(Dart_Handle library, + Dart_Handle error); + +/** + * Called by the embedder to load a partial program. Does not set the root + * library. + * + * \param kernel_buffer A buffer which contains a kernel binary (see + * pkg/kernel/binary.md). Must remain valid until isolate shutdown. + * \param kernel_buffer_size Length of the passed in buffer. + * + * \return A handle to the main library of the compilation unit, or an error. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_LoadLibraryFromKernel(const uint8_t* kernel_buffer, + intptr_t kernel_buffer_size); +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_LoadLibrary(Dart_Handle kernel_buffer); + +/** + * Indicates that all outstanding load requests have been satisfied. + * This finalizes all the new classes loaded and optionally completes + * deferred library futures. + * + * Requires there to be a current isolate. + * + * \param complete_futures Specify true if all deferred library + * futures should be completed, false otherwise. + * + * \return Success if all classes have been finalized and deferred library + * futures are completed. Otherwise, returns an error. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_FinalizeLoading(bool complete_futures); + +/* + * ===== + * Peers + * ===== + */ + +/** + * The peer field is a lazily allocated field intended for storage of + * an uncommonly used values. Most instances types can have a peer + * field allocated. The exceptions are subtypes of Null, num, and + * bool. + */ + +/** + * Returns the value of peer field of 'object' in 'peer'. + * + * \param object An object. + * \param peer An out parameter that returns the value of the peer + * field. + * + * \return Returns an error if 'object' is a subtype of Null, num, or + * bool. + */ +DART_EXPORT Dart_Handle Dart_GetPeer(Dart_Handle object, void** peer); + +/** + * Sets the value of the peer field of 'object' to the value of + * 'peer'. + * + * \param object An object. + * \param peer A value to store in the peer field. + * + * \return Returns an error if 'object' is a subtype of Null, num, or + * bool. + */ +DART_EXPORT Dart_Handle Dart_SetPeer(Dart_Handle object, void* peer); + +/* + * ====== + * Kernel + * ====== + */ + +/** + * Experimental support for Dart to Kernel parser isolate. + * + * TODO(hausner): Document finalized interface. + * + */ + +// TODO(33433): Remove kernel service from the embedding API. + +typedef enum { + Dart_KernelCompilationStatus_Unknown = -1, + Dart_KernelCompilationStatus_Ok = 0, + Dart_KernelCompilationStatus_Error = 1, + Dart_KernelCompilationStatus_Crash = 2, + Dart_KernelCompilationStatus_MsgFailed = 3, +} Dart_KernelCompilationStatus; + +typedef struct { + Dart_KernelCompilationStatus status; + bool null_safety; + char* error; + uint8_t* kernel; + intptr_t kernel_size; +} Dart_KernelCompilationResult; + +typedef enum { + Dart_KernelCompilationVerbosityLevel_Error = 0, + Dart_KernelCompilationVerbosityLevel_Warning, + Dart_KernelCompilationVerbosityLevel_Info, + Dart_KernelCompilationVerbosityLevel_All, +} Dart_KernelCompilationVerbosityLevel; + +DART_EXPORT bool Dart_IsKernelIsolate(Dart_Isolate isolate); +DART_EXPORT bool Dart_KernelIsolateIsRunning(void); +DART_EXPORT Dart_Port Dart_KernelPort(void); + +/** + * Compiles the given `script_uri` to a kernel file. + * + * \param platform_kernel A buffer containing the kernel of the platform (e.g. + * `vm_platform_strong.dill`). The VM does not take ownership of this memory. + * + * \param platform_kernel_size The length of the platform_kernel buffer. + * + * \param snapshot_compile Set to `true` when the compilation is for a snapshot. + * This is used by the frontend to determine if compilation related information + * should be printed to console (e.g., null safety mode). + * + * \param verbosity Specifies the logging behavior of the kernel compilation + * service. + * + * \return Returns the result of the compilation. + * + * On a successful compilation the returned [Dart_KernelCompilationResult] has + * a status of [Dart_KernelCompilationStatus_Ok] and the `kernel`/`kernel_size` + * fields are set. The caller takes ownership of the malloc()ed buffer. + * + * On a failed compilation the `error` might be set describing the reason for + * the failed compilation. The caller takes ownership of the malloc()ed + * error. + * + * Requires there to be a current isolate. + */ +DART_EXPORT Dart_KernelCompilationResult +Dart_CompileToKernel(const char* script_uri, + const uint8_t* platform_kernel, + const intptr_t platform_kernel_size, + bool incremental_compile, + bool snapshot_compile, + const char* package_config, + Dart_KernelCompilationVerbosityLevel verbosity); + +typedef struct { + const char* uri; + const char* source; +} Dart_SourceFile; + +DART_EXPORT Dart_KernelCompilationResult Dart_KernelListDependencies(void); + +/** + * Sets the kernel buffer which will be used to load Dart SDK sources + * dynamically at runtime. + * + * \param platform_kernel A buffer containing kernel which has sources for the + * Dart SDK populated. Note: The VM does not take ownership of this memory. + * + * \param platform_kernel_size The length of the platform_kernel buffer. + */ +DART_EXPORT void Dart_SetDartLibrarySourcesKernel( + const uint8_t* platform_kernel, + const intptr_t platform_kernel_size); + +/** + * Detect the null safety opt-in status. + * + * When running from source, it is based on the opt-in status of `script_uri`. + * When running from a kernel buffer, it is based on the mode used when + * generating `kernel_buffer`. + * When running from an appJIT or AOT snapshot, it is based on the mode used + * when generating `snapshot_data`. + * + * \param script_uri Uri of the script that contains the source code + * + * \param package_config Uri of the package configuration file (either in format + * of .packages or .dart_tool/package_config.json) for the null safety + * detection to resolve package imports against. If this parameter is not + * passed the package resolution of the parent isolate should be used. + * + * \param original_working_directory current working directory when the VM + * process was launched, this is used to correctly resolve the path specified + * for package_config. + * + * \param snapshot_data Buffer containing the snapshot data of the + * isolate or NULL if no snapshot is provided. If provided, the buffers must + * remain valid until the isolate shuts down. + * + * \param snapshot_instructions Buffer containing the snapshot instructions of + * the isolate or NULL if no snapshot is provided. If provided, the buffers + * must remain valid until the isolate shuts down. + * + * \param kernel_buffer A buffer which contains a kernel/DIL program. Must + * remain valid until isolate shutdown. + * + * \param kernel_buffer_size The size of `kernel_buffer`. + * + * \return Returns true if the null safety is opted in by the input being + * run `script_uri`, `snapshot_data` or `kernel_buffer`. + * + */ +DART_EXPORT bool Dart_DetectNullSafety(const char* script_uri, + const char* package_config, + const char* original_working_directory, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions, + const uint8_t* kernel_buffer, + intptr_t kernel_buffer_size); + +#define DART_KERNEL_ISOLATE_NAME "kernel-service" + +/* + * ======= + * Service + * ======= + */ + +#define DART_VM_SERVICE_ISOLATE_NAME "vm-service" + +/** + * Returns true if isolate is the service isolate. + * + * \param isolate An isolate + * + * \return Returns true if 'isolate' is the service isolate. + */ +DART_EXPORT bool Dart_IsServiceIsolate(Dart_Isolate isolate); + +/** + * Writes the CPU profile to the timeline as a series of 'instant' events. + * + * Note that this is an expensive operation. + * + * \param main_port The main port of the Isolate whose profile samples to write. + * \param error An optional error, must be free()ed by caller. + * + * \return Returns true if the profile is successfully written and false + * otherwise. + */ +DART_EXPORT bool Dart_WriteProfileToTimeline(Dart_Port main_port, char** error); + +/* + * ============== + * Precompilation + * ============== + */ + +/** + * Compiles all functions reachable from entry points and marks + * the isolate to disallow future compilation. + * + * Entry points should be specified using `@pragma("vm:entry-point")` + * annotation. + * + * \return An error handle if a compilation error or runtime error running const + * constructors was encountered. + */ +DART_EXPORT Dart_Handle Dart_Precompile(void); + +typedef void (*Dart_CreateLoadingUnitCallback)( + void* callback_data, + intptr_t loading_unit_id, + void** write_callback_data, + void** write_debug_callback_data); +typedef void (*Dart_StreamingWriteCallback)(void* callback_data, + const uint8_t* buffer, + intptr_t size); +typedef void (*Dart_StreamingCloseCallback)(void* callback_data); + +DART_EXPORT Dart_Handle Dart_LoadingUnitLibraryUris(intptr_t loading_unit_id); + +// On Darwin systems, 'dlsym' adds an '_' to the beginning of the symbol name. +// Use the '...CSymbol' definitions for resolving through 'dlsym'. The actual +// symbol names in the objects are given by the '...AsmSymbol' definitions. +#if defined(__APPLE__) +#define kSnapshotBuildIdCSymbol "kDartSnapshotBuildId" +#define kVmSnapshotDataCSymbol "kDartVmSnapshotData" +#define kVmSnapshotInstructionsCSymbol "kDartVmSnapshotInstructions" +#define kVmSnapshotBssCSymbol "kDartVmSnapshotBss" +#define kIsolateSnapshotDataCSymbol "kDartIsolateSnapshotData" +#define kIsolateSnapshotInstructionsCSymbol "kDartIsolateSnapshotInstructions" +#define kIsolateSnapshotBssCSymbol "kDartIsolateSnapshotBss" +#else +#define kSnapshotBuildIdCSymbol "_kDartSnapshotBuildId" +#define kVmSnapshotDataCSymbol "_kDartVmSnapshotData" +#define kVmSnapshotInstructionsCSymbol "_kDartVmSnapshotInstructions" +#define kVmSnapshotBssCSymbol "_kDartVmSnapshotBss" +#define kIsolateSnapshotDataCSymbol "_kDartIsolateSnapshotData" +#define kIsolateSnapshotInstructionsCSymbol "_kDartIsolateSnapshotInstructions" +#define kIsolateSnapshotBssCSymbol "_kDartIsolateSnapshotBss" +#endif + +#define kSnapshotBuildIdAsmSymbol "_kDartSnapshotBuildId" +#define kVmSnapshotDataAsmSymbol "_kDartVmSnapshotData" +#define kVmSnapshotInstructionsAsmSymbol "_kDartVmSnapshotInstructions" +#define kVmSnapshotBssAsmSymbol "_kDartVmSnapshotBss" +#define kIsolateSnapshotDataAsmSymbol "_kDartIsolateSnapshotData" +#define kIsolateSnapshotInstructionsAsmSymbol \ + "_kDartIsolateSnapshotInstructions" +#define kIsolateSnapshotBssAsmSymbol "_kDartIsolateSnapshotBss" + +/** + * Creates a precompiled snapshot. + * - A root library must have been loaded. + * - Dart_Precompile must have been called. + * + * Outputs an assembly file defining the symbols listed in the definitions + * above. + * + * The assembly should be compiled as a static or shared library and linked or + * loaded by the embedder. Running this snapshot requires a VM compiled with + * DART_PRECOMPILED_SNAPSHOT. The kDartVmSnapshotData and + * kDartVmSnapshotInstructions should be passed to Dart_Initialize. The + * kDartIsolateSnapshotData and kDartIsolateSnapshotInstructions should be + * passed to Dart_CreateIsolateGroup. + * + * The callback will be invoked one or more times to provide the assembly code. + * + * If stripped is true, then the assembly code will not include DWARF + * debugging sections. + * + * If debug_callback_data is provided, debug_callback_data will be used with + * the callback to provide separate debugging information. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateAppAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback, + void* callback_data, + bool stripped, + void* debug_callback_data); +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateAppAOTSnapshotAsAssemblies( + Dart_CreateLoadingUnitCallback next_callback, + void* next_callback_data, + bool stripped, + Dart_StreamingWriteCallback write_callback, + Dart_StreamingCloseCallback close_callback); + +/** + * Creates a precompiled snapshot. + * - A root library must have been loaded. + * - Dart_Precompile must have been called. + * + * Outputs an ELF shared library defining the symbols + * - _kDartVmSnapshotData + * - _kDartVmSnapshotInstructions + * - _kDartIsolateSnapshotData + * - _kDartIsolateSnapshotInstructions + * + * The shared library should be dynamically loaded by the embedder. + * Running this snapshot requires a VM compiled with DART_PRECOMPILED_SNAPSHOT. + * The kDartVmSnapshotData and kDartVmSnapshotInstructions should be passed to + * Dart_Initialize. The kDartIsolateSnapshotData and + * kDartIsolateSnapshotInstructions should be passed to Dart_CreateIsolate. + * + * The callback will be invoked one or more times to provide the binary output. + * + * If stripped is true, then the binary output will not include DWARF + * debugging sections. + * + * If debug_callback_data is provided, debug_callback_data will be used with + * the callback to provide separate debugging information. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateAppAOTSnapshotAsElf(Dart_StreamingWriteCallback callback, + void* callback_data, + bool stripped, + void* debug_callback_data); +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateAppAOTSnapshotAsElfs(Dart_CreateLoadingUnitCallback next_callback, + void* next_callback_data, + bool stripped, + Dart_StreamingWriteCallback write_callback, + Dart_StreamingCloseCallback close_callback); + +/** + * Like Dart_CreateAppAOTSnapshotAsAssembly, but only includes + * kDartVmSnapshotData and kDartVmSnapshotInstructions. It also does + * not strip DWARF information from the generated assembly or allow for + * separate debug information. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateVMAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback, + void* callback_data); + +/** + * Sorts the class-ids in depth first traversal order of the inheritance + * tree. This is a costly operation, but it can make method dispatch + * more efficient and is done before writing snapshots. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_SortClasses(void); + +/** + * Creates a snapshot that caches compiled code and type feedback for faster + * startup and quicker warmup in a subsequent process. + * + * Outputs a snapshot in two pieces. The pieces should be passed to + * Dart_CreateIsolateGroup in a VM using the same VM snapshot pieces used in the + * current VM. The instructions piece must be loaded with read and execute + * permissions; the data piece may be loaded as read-only. + * + * - Requires the VM to have not been started with --precompilation. + * - Not supported when targeting IA32. + * - The VM writing the snapshot and the VM reading the snapshot must be the + * same version, must be built in the same DEBUG/RELEASE/PRODUCT mode, must + * be targeting the same architecture, and must both be in checked mode or + * both in unchecked mode. + * + * The buffers are scope allocated and are only valid until the next call to + * Dart_ExitScope. + * + * \return A valid handle if no error occurs during the operation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateAppJITSnapshotAsBlobs(uint8_t** isolate_snapshot_data_buffer, + intptr_t* isolate_snapshot_data_size, + uint8_t** isolate_snapshot_instructions_buffer, + intptr_t* isolate_snapshot_instructions_size); + +/** + * Like Dart_CreateAppJITSnapshotAsBlobs, but also creates a new VM snapshot. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_CreateCoreJITSnapshotAsBlobs( + uint8_t** vm_snapshot_data_buffer, + intptr_t* vm_snapshot_data_size, + uint8_t** vm_snapshot_instructions_buffer, + intptr_t* vm_snapshot_instructions_size, + uint8_t** isolate_snapshot_data_buffer, + intptr_t* isolate_snapshot_data_size, + uint8_t** isolate_snapshot_instructions_buffer, + intptr_t* isolate_snapshot_instructions_size); + +/** + * Get obfuscation map for precompiled code. + * + * Obfuscation map is encoded as a JSON array of pairs (original name, + * obfuscated name). + * + * \return Returns an error handler if the VM was built in a mode that does not + * support obfuscation. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle +Dart_GetObfuscationMap(uint8_t** buffer, intptr_t* buffer_length); + +/** + * Returns whether the VM only supports running from precompiled snapshots and + * not from any other kind of snapshot or from source (that is, the VM was + * compiled with DART_PRECOMPILED_RUNTIME). + */ +DART_EXPORT bool Dart_IsPrecompiledRuntime(void); + +/** + * Print a native stack trace. Used for crash handling. + * + * If context is NULL, prints the current stack trace. Otherwise, context + * should be a CONTEXT* (Windows) or ucontext_t* (POSIX) from a signal handler + * running on the current thread. + */ +DART_EXPORT void Dart_DumpNativeStackTrace(void* context); + +/** + * Indicate that the process is about to abort, and the Dart VM should not + * attempt to cleanup resources. + */ +DART_EXPORT void Dart_PrepareToAbort(void); + +/** + * Callback provided by the embedder that is used by the VM to + * produce footnotes appended to DWARF stack traces. + * + * Whenever VM formats a stack trace as a string it would call this callback + * passing raw program counters for each frame in the stack trace. + * + * Embedder can then return a string which if not-null will be appended to the + * formatted stack trace. + * + * Returned string is expected to be `malloc()` allocated. VM takes ownership + * of the returned string and will `free()` it. + * + * \param addresses raw program counter addresses for each frame + * \param count number of elements in the addresses array + */ +typedef char* (*Dart_DwarfStackTraceFootnoteCallback)(void* addresses[], + intptr_t count); + +/** + * Configure DWARF stack trace footnote callback. + */ +DART_EXPORT void Dart_SetDwarfStackTraceFootnoteCallback( + Dart_DwarfStackTraceFootnoteCallback callback); + +#endif /* INCLUDE_DART_API_H_ */ /* NOLINT */ diff --git a/core/dart-bridge/include/dart_api_dl.c b/core/dart-bridge/include/dart_api_dl.c new file mode 100644 index 0000000..c4a68f4 --- /dev/null +++ b/core/dart-bridge/include/dart_api_dl.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#include "dart_api_dl.h" /* NOLINT */ +#include "dart_version.h" /* NOLINT */ +#include "internal/dart_api_dl_impl.h" /* NOLINT */ + +#include + +#define DART_API_DL_DEFINITIONS(name, R, A) name##_Type name##_DL = NULL; + +DART_API_ALL_DL_SYMBOLS(DART_API_DL_DEFINITIONS) + +#undef DART_API_DL_DEFINITIONS + +typedef void* DartApiEntry_function; + +DartApiEntry_function FindFunctionPointer(const DartApiEntry* entries, + const char* name) { + while (entries->name != NULL) { + if (strcmp(entries->name, name) == 0) return entries->function; + entries++; + } + return NULL; +} + +intptr_t Dart_InitializeApiDL(void* data) { + DartApi* dart_api_data = (DartApi*)data; + + if (dart_api_data->major != DART_API_DL_MAJOR_VERSION) { + // If the DartVM we're running on does not have the same version as this + // file was compiled against, refuse to initialize. The symbols are not + // compatible. + return -1; + } + // Minor versions are allowed to be different. + // If the DartVM has a higher minor version, it will provide more symbols + // than we initialize here. + // If the DartVM has a lower minor version, it will not provide all symbols. + // In that case, we leave the missing symbols un-initialized. Those symbols + // should not be used by the Dart and native code. The client is responsible + // for checking the minor version number himself based on which symbols it + // is using. + // (If we would error out on this case, recompiling native code against a + // newer SDK would break all uses on older SDKs, which is too strict.) + + const DartApiEntry* dart_api_function_pointers = dart_api_data->functions; + +#define DART_API_DL_INIT(name, R, A) \ + name##_DL = \ + (name##_Type)(FindFunctionPointer(dart_api_function_pointers, #name)); + DART_API_ALL_DL_SYMBOLS(DART_API_DL_INIT) +#undef DART_API_DL_INIT + + return 0; +} diff --git a/core/dart-bridge/include/dart_api_dl.h b/core/dart-bridge/include/dart_api_dl.h new file mode 100644 index 0000000..cce3450 --- /dev/null +++ b/core/dart-bridge/include/dart_api_dl.h @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_INCLUDE_DART_API_DL_H_ +#define RUNTIME_INCLUDE_DART_API_DL_H_ + +#include "dart_api.h" /* NOLINT */ +#include "dart_native_api.h" /* NOLINT */ + +/** \mainpage Dynamically Linked Dart API + * + * This exposes a subset of symbols from dart_api.h and dart_native_api.h + * available in every Dart embedder through dynamic linking. + * + * All symbols are postfixed with _DL to indicate that they are dynamically + * linked and to prevent conflicts with the original symbol. + * + * Link `dart_api_dl.c` file into your library and invoke + * `Dart_InitializeApiDL` with `NativeApi.initializeApiDLData`. + */ + +DART_EXPORT intptr_t Dart_InitializeApiDL(void* data); + +// ============================================================================ +// IMPORTANT! Never update these signatures without properly updating +// DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION. +// +// Verbatim copy of `dart_native_api.h` and `dart_api.h` symbol names and types +// to trigger compile-time errors if the symbols in those files are updated +// without updating these. +// +// Function return and argument types, and typedefs are carbon copied. Structs +// are typechecked nominally in C/C++, so they are not copied, instead a +// comment is added to their definition. +typedef int64_t Dart_Port_DL; + +typedef void (*Dart_NativeMessageHandler_DL)(Dart_Port_DL dest_port_id, + Dart_CObject* message); + +// dart_native_api.h symbols can be called on any thread. +#define DART_NATIVE_API_DL_SYMBOLS(F) \ + /***** dart_native_api.h *****/ \ + /* Dart_Port */ \ + F(Dart_PostCObject, bool, (Dart_Port_DL port_id, Dart_CObject * message)) \ + F(Dart_PostInteger, bool, (Dart_Port_DL port_id, int64_t message)) \ + F(Dart_NewNativePort, Dart_Port_DL, \ + (const char* name, Dart_NativeMessageHandler_DL handler, \ + bool handle_concurrently)) \ + F(Dart_CloseNativePort, bool, (Dart_Port_DL native_port_id)) + +// dart_api.h symbols can only be called on Dart threads. +#define DART_API_DL_SYMBOLS(F) \ + /***** dart_api.h *****/ \ + /* Errors */ \ + F(Dart_IsError, bool, (Dart_Handle handle)) \ + F(Dart_IsApiError, bool, (Dart_Handle handle)) \ + F(Dart_IsUnhandledExceptionError, bool, (Dart_Handle handle)) \ + F(Dart_IsCompilationError, bool, (Dart_Handle handle)) \ + F(Dart_IsFatalError, bool, (Dart_Handle handle)) \ + F(Dart_GetError, const char*, (Dart_Handle handle)) \ + F(Dart_ErrorHasException, bool, (Dart_Handle handle)) \ + F(Dart_ErrorGetException, Dart_Handle, (Dart_Handle handle)) \ + F(Dart_ErrorGetStackTrace, Dart_Handle, (Dart_Handle handle)) \ + F(Dart_NewApiError, Dart_Handle, (const char* error)) \ + F(Dart_NewCompilationError, Dart_Handle, (const char* error)) \ + F(Dart_NewUnhandledExceptionError, Dart_Handle, (Dart_Handle exception)) \ + F(Dart_PropagateError, void, (Dart_Handle handle)) \ + /* Dart_Handle, Dart_PersistentHandle, Dart_WeakPersistentHandle */ \ + F(Dart_HandleFromPersistent, Dart_Handle, (Dart_PersistentHandle object)) \ + F(Dart_HandleFromWeakPersistent, Dart_Handle, \ + (Dart_WeakPersistentHandle object)) \ + F(Dart_NewPersistentHandle, Dart_PersistentHandle, (Dart_Handle object)) \ + F(Dart_SetPersistentHandle, void, \ + (Dart_PersistentHandle obj1, Dart_Handle obj2)) \ + F(Dart_DeletePersistentHandle, void, (Dart_PersistentHandle object)) \ + F(Dart_NewWeakPersistentHandle, Dart_WeakPersistentHandle, \ + (Dart_Handle object, void* peer, intptr_t external_allocation_size, \ + Dart_HandleFinalizer callback)) \ + F(Dart_DeleteWeakPersistentHandle, void, (Dart_WeakPersistentHandle object)) \ + F(Dart_UpdateExternalSize, void, \ + (Dart_WeakPersistentHandle object, intptr_t external_allocation_size)) \ + F(Dart_NewFinalizableHandle, Dart_FinalizableHandle, \ + (Dart_Handle object, void* peer, intptr_t external_allocation_size, \ + Dart_HandleFinalizer callback)) \ + F(Dart_DeleteFinalizableHandle, void, \ + (Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object)) \ + F(Dart_UpdateFinalizableExternalSize, void, \ + (Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object, \ + intptr_t external_allocation_size)) \ + /* Isolates */ \ + F(Dart_CurrentIsolate, Dart_Isolate, (void)) \ + F(Dart_ExitIsolate, void, (void)) \ + F(Dart_EnterIsolate, void, (Dart_Isolate)) \ + /* Dart_Port */ \ + F(Dart_Post, bool, (Dart_Port_DL port_id, Dart_Handle object)) \ + F(Dart_NewSendPort, Dart_Handle, (Dart_Port_DL port_id)) \ + F(Dart_SendPortGetId, Dart_Handle, \ + (Dart_Handle port, Dart_Port_DL * port_id)) \ + /* Scopes */ \ + F(Dart_EnterScope, void, (void)) \ + F(Dart_ExitScope, void, (void)) \ + /* Objects */ \ + F(Dart_IsNull, bool, (Dart_Handle)) + +#define DART_API_ALL_DL_SYMBOLS(F) \ + DART_NATIVE_API_DL_SYMBOLS(F) \ + DART_API_DL_SYMBOLS(F) +// IMPORTANT! Never update these signatures without properly updating +// DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION. +// +// End of verbatim copy. +// ============================================================================ + +// Copy of definition of DART_EXPORT without 'used' attribute. +// +// The 'used' attribute cannot be used with DART_API_ALL_DL_SYMBOLS because +// they are not function declarations, but variable declarations with a +// function pointer type. +// +// The function pointer variables are initialized with the addresses of the +// functions in the VM. If we were to use function declarations instead, we +// would need to forward the call to the VM adding indirection. +#if defined(__CYGWIN__) +#error Tool chain and platform not supported. +#elif defined(_WIN32) +#if defined(DART_SHARED_LIB) +#define DART_EXPORT_DL DART_EXTERN_C __declspec(dllexport) +#else +#define DART_EXPORT_DL DART_EXTERN_C +#endif +#else +#if __GNUC__ >= 4 +#if defined(DART_SHARED_LIB) +#define DART_EXPORT_DL DART_EXTERN_C __attribute__((visibility("default"))) +#else +#define DART_EXPORT_DL DART_EXTERN_C +#endif +#else +#error Tool chain not supported. +#endif +#endif + +#define DART_API_DL_DECLARATIONS(name, R, A) \ + typedef R(*name##_Type) A; \ + DART_EXPORT_DL name##_Type name##_DL; + +DART_API_ALL_DL_SYMBOLS(DART_API_DL_DECLARATIONS) + +#undef DART_API_DL_DECLARATIONS + +#undef DART_EXPORT_DL + +#endif /* RUNTIME_INCLUDE_DART_API_DL_H_ */ /* NOLINT */ diff --git a/core/dart-bridge/include/dart_native_api.h b/core/dart-bridge/include/dart_native_api.h new file mode 100644 index 0000000..79194e0 --- /dev/null +++ b/core/dart-bridge/include/dart_native_api.h @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_INCLUDE_DART_NATIVE_API_H_ +#define RUNTIME_INCLUDE_DART_NATIVE_API_H_ + +#include "dart_api.h" /* NOLINT */ + +/* + * ========================================== + * Message sending/receiving from native code + * ========================================== + */ + +/** + * A Dart_CObject is used for representing Dart objects as native C + * data outside the Dart heap. These objects are totally detached from + * the Dart heap. Only a subset of the Dart objects have a + * representation as a Dart_CObject. + * + * The string encoding in the 'value.as_string' is UTF-8. + * + * All the different types from dart:typed_data are exposed as type + * kTypedData. The specific type from dart:typed_data is in the type + * field of the as_typed_data structure. The length in the + * as_typed_data structure is always in bytes. + * + * The data for kTypedData is copied on message send and ownership remains with + * the caller. The ownership of data for kExternalTyped is passed to the VM on + * message send and returned when the VM invokes the + * Dart_HandleFinalizer callback; a non-NULL callback must be provided. + * + * Note that Dart_CObject_kNativePointer is intended for internal use by + * dart:io implementation and has no connection to dart:ffi Pointer class. + * It represents a pointer to a native resource of a known type. + * The receiving side will only see this pointer as an integer and will not + * see the specified finalizer. + * The specified finalizer will only be invoked if the message is not delivered. + */ +typedef enum { + Dart_CObject_kNull = 0, + Dart_CObject_kBool, + Dart_CObject_kInt32, + Dart_CObject_kInt64, + Dart_CObject_kDouble, + Dart_CObject_kString, + Dart_CObject_kArray, + Dart_CObject_kTypedData, + Dart_CObject_kExternalTypedData, + Dart_CObject_kSendPort, + Dart_CObject_kCapability, + Dart_CObject_kNativePointer, + Dart_CObject_kUnsupported, + Dart_CObject_kUnmodifiableExternalTypedData, + Dart_CObject_kNumberOfTypes +} Dart_CObject_Type; +// This enum is versioned by DART_API_DL_MAJOR_VERSION, only add at the end +// and bump the DART_API_DL_MINOR_VERSION. + +typedef struct _Dart_CObject { + Dart_CObject_Type type; + union { + bool as_bool; + int32_t as_int32; + int64_t as_int64; + double as_double; + const char* as_string; + struct { + Dart_Port id; + Dart_Port origin_id; + } as_send_port; + struct { + int64_t id; + } as_capability; + struct { + intptr_t length; + struct _Dart_CObject** values; + } as_array; + struct { + Dart_TypedData_Type type; + intptr_t length; /* in elements, not bytes */ + const uint8_t* values; + } as_typed_data; + struct { + Dart_TypedData_Type type; + intptr_t length; /* in elements, not bytes */ + uint8_t* data; + void* peer; + Dart_HandleFinalizer callback; + } as_external_typed_data; + struct { + intptr_t ptr; + intptr_t size; + Dart_HandleFinalizer callback; + } as_native_pointer; + } value; +} Dart_CObject; +// This struct is versioned by DART_API_DL_MAJOR_VERSION, bump the version when +// changing this struct. + +/** + * Posts a message on some port. The message will contain the Dart_CObject + * object graph rooted in 'message'. + * + * While the message is being sent the state of the graph of Dart_CObject + * structures rooted in 'message' should not be accessed, as the message + * generation will make temporary modifications to the data. When the message + * has been sent the graph will be fully restored. + * + * If true is returned, the message was enqueued, and finalizers for external + * typed data will eventually run, even if the receiving isolate shuts down + * before processing the message. If false is returned, the message was not + * enqueued and ownership of external typed data in the message remains with the + * caller. + * + * This function may be called on any thread when the VM is running (that is, + * after Dart_Initialize has returned and before Dart_Cleanup has been called). + * + * \param port_id The destination port. + * \param message The message to send. + * + * \return True if the message was posted. + */ +DART_EXPORT bool Dart_PostCObject(Dart_Port port_id, Dart_CObject* message); + +/** + * Posts a message on some port. The message will contain the integer 'message'. + * + * \param port_id The destination port. + * \param message The message to send. + * + * \return True if the message was posted. + */ +DART_EXPORT bool Dart_PostInteger(Dart_Port port_id, int64_t message); + +/** + * A native message handler. + * + * This handler is associated with a native port by calling + * Dart_NewNativePort. + * + * The message received is decoded into the message structure. The + * lifetime of the message data is controlled by the caller. All the + * data references from the message are allocated by the caller and + * will be reclaimed when returning to it. + */ +typedef void (*Dart_NativeMessageHandler)(Dart_Port dest_port_id, + Dart_CObject* message); + +/** + * Creates a new native port. When messages are received on this + * native port, then they will be dispatched to the provided native + * message handler. + * + * \param name The name of this port in debugging messages. + * \param handler The C handler to run when messages arrive on the port. + * \param handle_concurrently Is it okay to process requests on this + * native port concurrently? + * + * \return If successful, returns the port id for the native port. In + * case of error, returns ILLEGAL_PORT. + */ +DART_EXPORT Dart_Port Dart_NewNativePort(const char* name, + Dart_NativeMessageHandler handler, + bool handle_concurrently); +/* TODO(turnidge): Currently handle_concurrently is ignored. */ + +/** + * Closes the native port with the given id. + * + * The port must have been allocated by a call to Dart_NewNativePort. + * + * \param native_port_id The id of the native port to close. + * + * \return Returns true if the port was closed successfully. + */ +DART_EXPORT bool Dart_CloseNativePort(Dart_Port native_port_id); + +/* + * ================== + * Verification Tools + * ================== + */ + +/** + * Forces all loaded classes and functions to be compiled eagerly in + * the current isolate.. + * + * TODO(turnidge): Document. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CompileAll(void); + +/** + * Finalizes all classes. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_FinalizeAllClasses(void); + +/* This function is intentionally undocumented. + * + * It should not be used outside internal tests. + */ +DART_EXPORT void* Dart_ExecuteInternalCommand(const char* command, void* arg); + +#endif /* INCLUDE_DART_NATIVE_API_H_ */ /* NOLINT */ diff --git a/core/dart-bridge/include/dart_tools_api.h b/core/dart-bridge/include/dart_tools_api.h new file mode 100644 index 0000000..7b706bc --- /dev/null +++ b/core/dart-bridge/include/dart_tools_api.h @@ -0,0 +1,658 @@ +// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#ifndef RUNTIME_INCLUDE_DART_TOOLS_API_H_ +#define RUNTIME_INCLUDE_DART_TOOLS_API_H_ + +#include "dart_api.h" /* NOLINT */ + +/** \mainpage Dart Tools Embedding API Reference + * + * This reference describes the Dart embedding API for tools. Tools include + * a debugger, service protocol, and timeline. + * + * NOTE: The APIs described in this file are unstable and subject to change. + * + * This reference is generated from the header include/dart_tools_api.h. + */ + +/* + * ======== + * Debugger + * ======== + */ + +/** + * ILLEGAL_ISOLATE_ID is a number guaranteed never to be associated with a + * valid isolate. + */ +#define ILLEGAL_ISOLATE_ID ILLEGAL_PORT + +/** + * ILLEGAL_ISOLATE_GROUP_ID is a number guaranteed never to be associated with a + * valid isolate group. + */ +#define ILLEGAL_ISOLATE_GROUP_ID 0 + +/* + * ======= + * Service + * ======= + */ + +/** + * A service request callback function. + * + * These callbacks, registered by the embedder, are called when the VM receives + * a service request it can't handle and the service request command name + * matches one of the embedder registered handlers. + * + * The return value of the callback indicates whether the response + * should be used as a regular result or an error result. + * Specifically, if the callback returns true, a regular JSON-RPC + * response is built in the following way: + * + * { + * "jsonrpc": "2.0", + * "result": , + * "id": , + * } + * + * If the callback returns false, a JSON-RPC error is built like this: + * + * { + * "jsonrpc": "2.0", + * "error": , + * "id": , + * } + * + * \param method The rpc method name. + * \param param_keys Service requests can have key-value pair parameters. The + * keys and values are flattened and stored in arrays. + * \param param_values The values associated with the keys. + * \param num_params The length of the param_keys and param_values arrays. + * \param user_data The user_data pointer registered with this handler. + * \param result A C string containing a valid JSON object. The returned + * pointer will be freed by the VM by calling free. + * + * \return True if the result is a regular JSON-RPC response, false if the + * result is a JSON-RPC error. + */ +typedef bool (*Dart_ServiceRequestCallback)(const char* method, + const char** param_keys, + const char** param_values, + intptr_t num_params, + void* user_data, + const char** json_object); + +/** + * Register a Dart_ServiceRequestCallback to be called to handle + * requests for the named rpc on a specific isolate. The callback will + * be invoked with the current isolate set to the request target. + * + * \param method The name of the method that this callback is responsible for. + * \param callback The callback to invoke. + * \param user_data The user data passed to the callback. + * + * NOTE: If multiple callbacks with the same name are registered, only + * the last callback registered will be remembered. + */ +DART_EXPORT void Dart_RegisterIsolateServiceRequestCallback( + const char* method, + Dart_ServiceRequestCallback callback, + void* user_data); + +/** + * Register a Dart_ServiceRequestCallback to be called to handle + * requests for the named rpc. The callback will be invoked without a + * current isolate. + * + * \param method The name of the command that this callback is responsible for. + * \param callback The callback to invoke. + * \param user_data The user data passed to the callback. + * + * NOTE: If multiple callbacks with the same name are registered, only + * the last callback registered will be remembered. + */ +DART_EXPORT void Dart_RegisterRootServiceRequestCallback( + const char* method, + Dart_ServiceRequestCallback callback, + void* user_data); + +/** + * Embedder information which can be requested by the VM for internal or + * reporting purposes. + * + * The pointers in this structure are not going to be cached or freed by the VM. + */ + + #define DART_EMBEDDER_INFORMATION_CURRENT_VERSION (0x00000001) + +typedef struct { + int32_t version; + const char* name; // [optional] The name of the embedder + int64_t current_rss; // [optional] the current RSS of the embedder + int64_t max_rss; // [optional] the maximum RSS of the embedder +} Dart_EmbedderInformation; + +/** + * Callback provided by the embedder that is used by the VM to request + * information. + * + * \return Returns a pointer to a Dart_EmbedderInformation structure. + * The embedder keeps the ownership of the structure and any field in it. + * The embedder must ensure that the structure will remain valid until the + * next invocation of the callback. + */ +typedef void (*Dart_EmbedderInformationCallback)( + Dart_EmbedderInformation* info); + +/** + * Register a Dart_ServiceRequestCallback to be called to handle + * requests for the named rpc. The callback will be invoked without a + * current isolate. + * + * \param method The name of the command that this callback is responsible for. + * \param callback The callback to invoke. + * \param user_data The user data passed to the callback. + * + * NOTE: If multiple callbacks are registered, only the last callback registered + * will be remembered. + */ +DART_EXPORT void Dart_SetEmbedderInformationCallback( + Dart_EmbedderInformationCallback callback); + +/** + * Invoke a vm-service method and wait for its result. + * + * \param request_json The utf8-encoded json-rpc request. + * \param request_json_length The length of the json-rpc request. + * + * \param response_json The returned utf8-encoded json response, must be + * free()ed by caller. + * \param response_json_length The length of the returned json response. + * \param error An optional error, must be free()ed by caller. + * + * \return Whether the call was successfully performed. + * + * NOTE: This method does not need a current isolate and must not have the + * vm-isolate being the current isolate. It must be called after + * Dart_Initialize() and before Dart_Cleanup(). + */ +DART_EXPORT bool Dart_InvokeVMServiceMethod(uint8_t* request_json, + intptr_t request_json_length, + uint8_t** response_json, + intptr_t* response_json_length, + char** error); + +/* + * ======== + * Event Streams + * ======== + */ + +/** + * A callback invoked when the VM service gets a request to listen to + * some stream. + * + * \return Returns true iff the embedder supports the named stream id. + */ +typedef bool (*Dart_ServiceStreamListenCallback)(const char* stream_id); + +/** + * A callback invoked when the VM service gets a request to cancel + * some stream. + */ +typedef void (*Dart_ServiceStreamCancelCallback)(const char* stream_id); + +/** + * Adds VM service stream callbacks. + * + * \param listen_callback A function pointer to a listen callback function. + * A listen callback function should not be already set when this function + * is called. A NULL value removes the existing listen callback function + * if any. + * + * \param cancel_callback A function pointer to a cancel callback function. + * A cancel callback function should not be already set when this function + * is called. A NULL value removes the existing cancel callback function + * if any. + * + * \return Success if the callbacks were added. Otherwise, returns an + * error handle. + */ +DART_EXPORT char* Dart_SetServiceStreamCallbacks( + Dart_ServiceStreamListenCallback listen_callback, + Dart_ServiceStreamCancelCallback cancel_callback); + +/** + * Sends a data event to clients of the VM Service. + * + * A data event is used to pass an array of bytes to subscribed VM + * Service clients. For example, in the standalone embedder, this is + * function used to provide WriteEvents on the Stdout and Stderr + * streams. + * + * If the embedder passes in a stream id for which no client is + * subscribed, then the event is ignored. + * + * \param stream_id The id of the stream on which to post the event. + * + * \param event_kind A string identifying what kind of event this is. + * For example, 'WriteEvent'. + * + * \param bytes A pointer to an array of bytes. + * + * \param bytes_length The length of the byte array. + * + * \return NULL if the arguments are well formed. Otherwise, returns an + * error string. The caller is responsible for freeing the error message. + */ +DART_EXPORT char* Dart_ServiceSendDataEvent(const char* stream_id, + const char* event_kind, + const uint8_t* bytes, + intptr_t bytes_length); + +/* + * ======== + * Reload support + * ======== + * + * These functions are used to implement reloading in the Dart VM. + * This is an experimental feature, so embedders should be prepared + * for these functions to change. + */ + +/** + * A callback which determines whether the file at some url has been + * modified since some time. If the file cannot be found, true should + * be returned. + */ +typedef bool (*Dart_FileModifiedCallback)(const char* url, int64_t since); + +DART_EXPORT char* Dart_SetFileModifiedCallback( + Dart_FileModifiedCallback file_modified_callback); + +/** + * Returns true if isolate is currently reloading. + */ +DART_EXPORT bool Dart_IsReloading(); + +/* + * ======== + * Timeline + * ======== + */ + +/** + * Enable tracking of specified timeline category. This is operational + * only when systrace timeline functionality is turned on. + * + * \param categories A comma separated list of categories that need to + * be enabled, the categories are + * "all" : All categories + * "API" - Execution of Dart C API functions + * "Compiler" - Execution of Dart JIT compiler + * "CompilerVerbose" - More detailed Execution of Dart JIT compiler + * "Dart" - Execution of Dart code + * "Debugger" - Execution of Dart debugger + * "Embedder" - Execution of Dart embedder code + * "GC" - Execution of Dart Garbage Collector + * "Isolate" - Dart Isolate lifecycle execution + * "VM" - Execution in Dart VM runtime code + * "" - None + * + * When "all" is specified all the categories are enabled. + * When a comma separated list of categories is specified, the categories + * that are specified will be enabled and the rest will be disabled. + * When "" is specified all the categories are disabled. + * The category names are case sensitive. + * eg: Dart_EnableTimelineCategory("all"); + * Dart_EnableTimelineCategory("GC,API,Isolate"); + * Dart_EnableTimelineCategory("GC,Debugger,Dart"); + * + * \return True if the categories were successfully enabled, False otherwise. + */ +DART_EXPORT bool Dart_SetEnabledTimelineCategory(const char* categories); + +/** + * Returns a timestamp in microseconds. This timestamp is suitable for + * passing into the timeline system, and uses the same monotonic clock + * as dart:developer's Timeline.now. + * + * \return A timestamp that can be passed to the timeline system. + */ +DART_EXPORT int64_t Dart_TimelineGetMicros(); + +/** + * Returns a raw timestamp in from the monotonic clock. + * + * \return A raw timestamp from the monotonic clock. + */ +DART_EXPORT int64_t Dart_TimelineGetTicks(); + +/** + * Returns the frequency of the monotonic clock. + * + * \return The frequency of the monotonic clock. + */ +DART_EXPORT int64_t Dart_TimelineGetTicksFrequency(); + +typedef enum { + Dart_Timeline_Event_Begin, // Phase = 'B'. + Dart_Timeline_Event_End, // Phase = 'E'. + Dart_Timeline_Event_Instant, // Phase = 'i'. + Dart_Timeline_Event_Duration, // Phase = 'X'. + Dart_Timeline_Event_Async_Begin, // Phase = 'b'. + Dart_Timeline_Event_Async_End, // Phase = 'e'. + Dart_Timeline_Event_Async_Instant, // Phase = 'n'. + Dart_Timeline_Event_Counter, // Phase = 'C'. + Dart_Timeline_Event_Flow_Begin, // Phase = 's'. + Dart_Timeline_Event_Flow_Step, // Phase = 't'. + Dart_Timeline_Event_Flow_End, // Phase = 'f'. +} Dart_Timeline_Event_Type; + +/** + * Add a timeline event to the embedder stream. + * + * DEPRECATED: this function will be removed in Dart SDK v3.2. + * + * \param label The name of the event. Its lifetime must extend at least until + * Dart_Cleanup. + * \param timestamp0 The first timestamp of the event. + * \param timestamp1_or_id When reporting an event of type + * |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event + * should be passed through |timestamp1_or_id|. When reporting an event of + * type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|, + * or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the + * event should be passed through |timestamp1_or_id|. When reporting an + * event of type |Dart_Timeline_Event_Flow_Begin|, + * |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the + * flow ID associated with the event should be passed through + * |timestamp1_or_id|. When reporting an event of type + * |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID + * associated with the event should be passed through |timestamp1_or_id|. + * Note that this event ID will only be used by the MacOS recorder. The + * argument to |timestamp1_or_id| will not be used when reporting events of + * other types. + * \param argument_count The number of argument names and values. + * \param argument_names An array of names of the arguments. The lifetime of the + * names must extend at least until Dart_Cleanup. The array may be reclaimed + * when this call returns. + * \param argument_values An array of values of the arguments. The values and + * the array may be reclaimed when this call returns. + */ +DART_EXPORT void Dart_TimelineEvent(const char* label, + int64_t timestamp0, + int64_t timestamp1_or_id, + Dart_Timeline_Event_Type type, + intptr_t argument_count, + const char** argument_names, + const char** argument_values); + +/** + * Add a timeline event to the embedder stream. + * + * Note regarding flow events: events must be associated with flow IDs in two + * different ways to allow flow events to be serialized correctly in both + * Chrome's JSON trace event format and Perfetto's proto trace format. Events + * of type |Dart_Timeline_Event_Flow_Begin|, |Dart_Timeline_Event_Flow_Step|, + * and |Dart_Timeline_Event_Flow_End| must be reported to support serialization + * in Chrome's trace format. The |flow_ids| argument must be supplied when + * reporting events of type |Dart_Timeline_Event_Begin|, + * |Dart_Timeline_Event_Duration|, |Dart_Timeline_Event_Instant|, + * |Dart_Timeline_Event_Async_Begin|, and |Dart_Timeline_Event_Async_Instant| to + * support serialization in Perfetto's proto format. + * + * \param label The name of the event. Its lifetime must extend at least until + * Dart_Cleanup. + * \param timestamp0 The first timestamp of the event. + * \param timestamp1_or_id When reporting an event of type + * |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event + * should be passed through |timestamp1_or_id|. When reporting an event of + * type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|, + * or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the + * event should be passed through |timestamp1_or_id|. When reporting an + * event of type |Dart_Timeline_Event_Flow_Begin|, + * |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the + * flow ID associated with the event should be passed through + * |timestamp1_or_id|. When reporting an event of type + * |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID + * associated with the event should be passed through |timestamp1_or_id|. + * Note that this event ID will only be used by the MacOS recorder. The + * argument to |timestamp1_or_id| will not be used when reporting events of + * other types. + * \param flow_id_count The number of flow IDs associated with this event. + * \param flow_ids An array of flow IDs associated with this event. The array + * may be reclaimed when this call returns. + * \param argument_count The number of argument names and values. + * \param argument_names An array of names of the arguments. The lifetime of the + * names must extend at least until Dart_Cleanup. The array may be reclaimed + * when this call returns. + * \param argument_values An array of values of the arguments. The values and + * the array may be reclaimed when this call returns. + */ +DART_EXPORT void Dart_RecordTimelineEvent(const char* label, + int64_t timestamp0, + int64_t timestamp1_or_id, + intptr_t flow_id_count, + const int64_t* flow_ids, + Dart_Timeline_Event_Type type, + intptr_t argument_count, + const char** argument_names, + const char** argument_values); + +/** + * Associates a name with the current thread. This name will be used to name + * threads in the timeline. Can only be called after a call to Dart_Initialize. + * + * \param name The name of the thread. + */ +DART_EXPORT void Dart_SetThreadName(const char* name); + +typedef struct { + const char* name; + const char* value; +} Dart_TimelineRecorderEvent_Argument; + +#define DART_TIMELINE_RECORDER_CURRENT_VERSION (0x00000002) + +typedef struct { + /* Set to DART_TIMELINE_RECORDER_CURRENT_VERSION */ + int32_t version; + + /* The event's type / phase. */ + Dart_Timeline_Event_Type type; + + /* The event's timestamp according to the same clock as + * Dart_TimelineGetMicros. For a duration event, this is the beginning time. + */ + int64_t timestamp0; + + /** + * For a duration event, this is the end time. For an async event, this is the + * async ID. For a flow event, this is the flow ID. For a begin or end event, + * this is the event ID (which is only referenced by the MacOS recorder). + */ + int64_t timestamp1_or_id; + + /* The current isolate of the event, as if by Dart_GetMainPortId, or + * ILLEGAL_PORT if the event had no current isolate. */ + Dart_Port isolate; + + /* The current isolate group of the event, as if by + * Dart_CurrentIsolateGroupId, or ILLEGAL_PORT if the event had no current + * isolate group. */ + Dart_IsolateGroupId isolate_group; + + /* The callback data associated with the isolate if any. */ + void* isolate_data; + + /* The callback data associated with the isolate group if any. */ + void* isolate_group_data; + + /* The name / label of the event. */ + const char* label; + + /* The stream / category of the event. */ + const char* stream; + + intptr_t argument_count; + Dart_TimelineRecorderEvent_Argument* arguments; +} Dart_TimelineRecorderEvent; + +/** + * Callback provided by the embedder to handle the completion of timeline + * events. + * + * \param event A timeline event that has just been completed. The VM keeps + * ownership of the event and any field in it (i.e., the embedder should copy + * any values it needs after the callback returns). + */ +typedef void (*Dart_TimelineRecorderCallback)( + Dart_TimelineRecorderEvent* event); + +/** + * Register a `Dart_TimelineRecorderCallback` to be called as timeline events + * are completed. + * + * The callback will be invoked without a current isolate. + * + * The callback will be invoked on the thread completing the event. Because + * `Dart_TimelineEvent` may be called by any thread, the callback may be called + * on any thread. + * + * The callback may be invoked at any time after `Dart_Initialize` is called and + * before `Dart_Cleanup` returns. + * + * If multiple callbacks are registered, only the last callback registered + * will be remembered. Providing a NULL callback will clear the registration + * (i.e., a NULL callback produced a no-op instead of a crash). + * + * Setting a callback is insufficient to receive events through the callback. The + * VM flag `timeline_recorder` must also be set to `callback`. + */ +DART_EXPORT void Dart_SetTimelineRecorderCallback( + Dart_TimelineRecorderCallback callback); + +/* + * ======= + * Metrics + * ======= + */ + +/** + * Return metrics gathered for the VM and individual isolates. + */ +DART_EXPORT int64_t +Dart_IsolateGroupHeapOldUsedMetric(Dart_IsolateGroup group); // Byte +DART_EXPORT int64_t +Dart_IsolateGroupHeapOldCapacityMetric(Dart_IsolateGroup group); // Byte +DART_EXPORT int64_t +Dart_IsolateGroupHeapOldExternalMetric(Dart_IsolateGroup group); // Byte +DART_EXPORT int64_t +Dart_IsolateGroupHeapNewUsedMetric(Dart_IsolateGroup group); // Byte +DART_EXPORT int64_t +Dart_IsolateGroupHeapNewCapacityMetric(Dart_IsolateGroup group); // Byte +DART_EXPORT int64_t +Dart_IsolateGroupHeapNewExternalMetric(Dart_IsolateGroup group); // Byte + +/* + * ======== + * UserTags + * ======== + */ + +/* + * Gets the current isolate's currently set UserTag instance. + * + * \return The currently set UserTag instance. + */ +DART_EXPORT Dart_Handle Dart_GetCurrentUserTag(); + +/* + * Gets the current isolate's default UserTag instance. + * + * \return The default UserTag with label 'Default' + */ +DART_EXPORT Dart_Handle Dart_GetDefaultUserTag(); + +/* + * Creates a new UserTag instance. + * + * \param label The name of the new UserTag. + * + * \return The newly created UserTag instance or an error handle. + */ +DART_EXPORT Dart_Handle Dart_NewUserTag(const char* label); + +/* + * Updates the current isolate's UserTag to a new value. + * + * \param user_tag The UserTag to be set as the current UserTag. + * + * \return The previously set UserTag instance or an error handle. + */ +DART_EXPORT Dart_Handle Dart_SetCurrentUserTag(Dart_Handle user_tag); + +/* + * Returns the label of a given UserTag instance. + * + * \param user_tag The UserTag from which the label will be retrieved. + * + * \return The UserTag's label. NULL if the user_tag is invalid. The caller is + * responsible for freeing the returned label. + */ +DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_GetUserTagLabel( + Dart_Handle user_tag); + +/* + * ======= + * Heap Snapshot + * ======= + */ + +/** + * Callback provided by the caller of `Dart_WriteHeapSnapshot` which is + * used to write out chunks of the requested heap snapshot. + * + * \param context An opaque context which was passed to `Dart_WriteHeapSnapshot` + * together with this callback. + * + * \param buffer Pointer to the buffer containing a chunk of the snapshot. + * The callback owns the buffer and needs to `free` it. + * + * \param size Number of bytes in the `buffer` to be written. + * + * \param is_last Set to `true` for the last chunk. The callback will not + * be invoked again after it was invoked once with `is_last` set to `true`. + */ +typedef void (*Dart_HeapSnapshotWriteChunkCallback)(void* context, + uint8_t* buffer, + intptr_t size, + bool is_last); + +/** + * Generate heap snapshot of the current isolate group and stream it into the + * given `callback`. VM would produce snapshot in chunks and send these chunks + * one by one back to the embedder by invoking the provided `callback`. + * + * This API enables embedder to stream snapshot into a file or socket without + * allocating a buffer to hold the whole snapshot in memory. + * + * The isolate group will be paused for the duration of this operation. + * + * \param write Callback used to write chunks of the heap snapshot. + * + * \param context Opaque context which would be passed on each invocation of + * `write` callback. + * + * \returns `nullptr` if the operation is successful otherwise error message. + * Caller owns error message string and needs to `free` it. + */ +DART_EXPORT char* Dart_WriteHeapSnapshot( + Dart_HeapSnapshotWriteChunkCallback write, + void* context); + +#endif // RUNTIME_INCLUDE_DART_TOOLS_API_H_ diff --git a/core/dart-bridge/include/dart_version.h b/core/dart-bridge/include/dart_version.h new file mode 100644 index 0000000..e2d3651 --- /dev/null +++ b/core/dart-bridge/include/dart_version.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_INCLUDE_DART_VERSION_H_ +#define RUNTIME_INCLUDE_DART_VERSION_H_ + +// On breaking changes the major version is increased. +// On backwards compatible changes the minor version is increased. +// The versioning covers the symbols exposed in dart_api_dl.h +#define DART_API_DL_MAJOR_VERSION 2 +#define DART_API_DL_MINOR_VERSION 3 + +#endif /* RUNTIME_INCLUDE_DART_VERSION_H_ */ /* NOLINT */ diff --git a/core/dart-bridge/include/internal/dart_api_dl_impl.h b/core/dart-bridge/include/internal/dart_api_dl_impl.h new file mode 100644 index 0000000..e4a5689 --- /dev/null +++ b/core/dart-bridge/include/internal/dart_api_dl_impl.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ +#define RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ + +typedef struct { + const char* name; + void (*function)(void); +} DartApiEntry; + +typedef struct { + const int major; + const int minor; + const DartApiEntry* const functions; +} DartApi; + +#endif /* RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ */ /* NOLINT */ diff --git a/core/dart-bridge/lib.go b/core/dart-bridge/lib.go new file mode 100644 index 0000000..ca162a3 --- /dev/null +++ b/core/dart-bridge/lib.go @@ -0,0 +1,37 @@ +package dart_bridge + +/* +#include "stdint.h" +#include "include/dart_api_dl.h" +#include "include/dart_api_dl.c" +#include "include/dart_native_api.h" + +bool GoDart_PostCObject(Dart_Port_DL port, Dart_CObject* obj) { + return Dart_PostCObject_DL(port, obj); +} +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +func InitDartApi(api unsafe.Pointer) { + if C.Dart_InitializeApiDL(api) != 0 { + panic("failed to create dart bridge") + } else { + fmt.Println("Dart Api DL is initialized") + } +} + +func sendToPort(port int64, msg string) { + var obj C.Dart_CObject + obj._type = C.Dart_CObject_kString + msgString := C.CString(msg) + ptr := unsafe.Pointer(&obj.value[0]) + *(**C.char)(ptr) = msgString + isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) + if !isSuccess { + fmt.Println("ERROR: post to port ", port, " failed", msg) + } +} diff --git a/core/dart-bridge/message.go b/core/dart-bridge/message.go new file mode 100644 index 0000000..8fc5227 --- /dev/null +++ b/core/dart-bridge/message.go @@ -0,0 +1,28 @@ +package dart_bridge + +import "encoding/json" + +var Port *int64 + +type MessageType string + +const ( + Log MessageType = "log" + Tun MessageType = "tun" + Delay MessageType = "delay" + Process MessageType = "process" +) + +type Message struct { + Type MessageType `json:"type"` + Data interface{} `json:"data"` +} + +func (message *Message) toJson() string { + data, _ := json.Marshal(message) + return string(data) +} + +func SendMessage(message Message) { + sendToPort(*Port, message.toJson()) +} diff --git a/core/go.mod b/core/go.mod new file mode 100644 index 0000000..6c14221 --- /dev/null +++ b/core/go.mod @@ -0,0 +1,107 @@ +module core + +go 1.21.0 + +replace github.com/metacubex/mihomo => ./Clash.Meta + +require ( + github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 + github.com/metacubex/mihomo v1.17.1 + github.com/miekg/dns v1.1.59 + golang.org/x/net v0.24.0 + golang.org/x/sync v0.7.0 +) + +require ( + github.com/3andne/restls-client-go v0.1.6 // indirect + github.com/RyuaNerin/go-krypto v1.2.4 // indirect + github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cilium/ebpf v0.12.3 // indirect + github.com/cloudflare/circl v1.3.6 // indirect + github.com/coreos/go-iptables v0.7.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect + github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect + github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect + github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gaukas/godicttls v0.0.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.3.2 // indirect + github.com/gofrs/uuid/v5 v5.1.0 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect + github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect + github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 // indirect + github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d // indirect + github.com/metacubex/sing-shadowsocks v0.2.6 // indirect + github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect + github.com/metacubex/sing-tun v0.2.6 // indirect + github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect + github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 // indirect + github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect + github.com/mroth/weightedrand/v2 v2.1.0 // indirect + github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/openacid/low v0.1.21 // indirect + github.com/oschwald/maxminddb-golang v1.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect + github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect + github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect + github.com/sagernet/sing v0.3.8 // indirect + github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect + github.com/sagernet/sing-shadowtls v0.1.4 // indirect + github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect + github.com/sagernet/utls v1.5.4 // indirect + github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect + github.com/samber/lo v1.39.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.3 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect + github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect + github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zhangyunhao116/fastrand v0.4.0 // indirect + gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect + go.uber.org/mock v0.4.0 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.20.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.2.2 // indirect +) diff --git a/core/go.sum b/core/go.sum new file mode 100644 index 0000000..064ded5 --- /dev/null +++ b/core/go.sum @@ -0,0 +1,281 @@ +github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= +github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= +github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 h1:USCTqih5d1bUXUxWNS9ZD5Tx/lb0jXHEtRIIx/F9dMc= +github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34/go.mod h1:YR9wK13TgI5ww8iKWm91MHiSoHC7Oz0U4beCCmtXqLw= +github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= +github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= +github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= +github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= +github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= +github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= +github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= +github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= +github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= +github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= +github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI= +github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= +github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= +github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= +github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= +github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= +github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk= +github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= +github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49 h1:/OuvSMGT9+xnyZ+7MZQ1zdngaCCAdPoSw8B/uurZ7pg= +github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= +github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= +github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= +github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw= +github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 h1:oMLlJV4a9AylNo8ZLBNUiqZ02Vme6GLLHjuWJz8amSk= +github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98/go.mod h1:iGx3Y1zynls/FjFgykLSqDcM81U0IKePRTXEz5g3iiQ= +github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d h1:RAe0ND8J5SOPGI623oEXfaHKaaUrrzHx+U1DN9Awcco= +github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8= +github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= +github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= +github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= +github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= +github.com/metacubex/sing-tun v0.2.6 h1:frc58BqnIClqcC9KcYBfVAn5bgO6WW1ANKvZW3/HYAQ= +github.com/metacubex/sing-tun v0.2.6/go.mod h1:4VsMwZH1IlgPGFK1ZbBomZ/B2MYkTgs2+gnBAr5GOIo= +github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= +github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= +github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 h1:AGyIB55UfQm/0ZH0HtQO9u3l//yjtHUpjeRjjPGfGRI= +github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= +github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= +github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= +github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= +github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= +github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= +github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= +github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= +github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= +github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= +github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= +github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= +github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.3.8 h1:gm4JKalPhydMYX2zFOTnnd4TXtM/16WFRqSjMepYQQk= +github.com/sagernet/sing v0.3.8/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI= +github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14= +github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= +github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= +github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= +github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= +github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= +github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= +github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= +github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo= +github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE= +github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= +github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= +github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= +github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= +github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= +github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zhangyunhao116/fastrand v0.4.0 h1:86QB6Y+GGgLZRFRDCjMmAS28QULwspK9sgL5d1Bx3H4= +github.com/zhangyunhao116/fastrand v0.4.0/go.mod h1:vIyo6EyBhjGKpZv6qVlkPl4JVAklpMM4DSKzbAkMguA= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.2.2 h1:wEAbSg0IVU4ih44CVlpMqMZMpzr5hf/6aqodLlevd/w= +lukechampine.com/blake3 v1.2.2/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/core/hub.go b/core/hub.go new file mode 100644 index 0000000..c212734 --- /dev/null +++ b/core/hub.go @@ -0,0 +1,255 @@ +package main + +import "C" +import ( + bridge "core/dart-bridge" + "encoding/json" + "fmt" + "github.com/metacubex/mihomo/adapter" + "github.com/metacubex/mihomo/adapter/outboundgroup" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/config" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/hub/executor" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/tunnel" + "github.com/metacubex/mihomo/tunnel/statistic" + "golang.org/x/net/context" + "os" + "runtime" + "time" + "unsafe" +) + +var currentConfig *config.RawConfig + +var isInit = false + +//export initClash +func initClash(homeDirStr *C.char) bool { + if !isInit { + constant.SetHomeDir(C.GoString(homeDirStr)) + isInit = true + } + return isInit +} + +//export getIsInit +func getIsInit() bool { + return isInit +} + +//export restartClash +func restartClash() bool { + execPath, _ := os.Executable() + go restartExecutable(execPath) + return true +} + +//export shutdownClash +func shutdownClash() bool { + executor.Shutdown() + runtime.GC() + isInit = false + currentConfig = nil + return true +} + +//export validateConfig +func validateConfig(s *C.char) bool { + bytes := []byte(C.GoString(s)) + _, err := config.UnmarshalRawConfig(bytes) + return err == nil +} + +//export updateConfig +func updateConfig(s *C.char) bool { + paramsString := C.GoString(s) + var params = &GenerateConfigParams{} + err := json.Unmarshal([]byte(paramsString), params) + if err != nil { + log.Errorln("generateConfig Unmarshal error %v", err) + return false + } + prof := decorationConfig(params.ProfilePath, *params.Config) + currentConfig = prof + if *params.IsPatch { + return applyConfig(true) + } else { + return applyConfig(false) + } +} + +//export getProxies +func getProxies() *C.char { + data, err := json.Marshal(tunnel.Proxies()) + if err != nil { + return C.CString("") + } + return C.CString(string(data)) +} + +//export changeProxy +func changeProxy(s *C.char) bool { + paramsString := C.GoString(s) + var params = &ChangeProxyParams{} + err := json.Unmarshal([]byte(paramsString), params) + if err != nil { + log.Infoln("Unmarshal ChangeProxyParams %v", err) + return false + } + proxies := tunnel.Proxies() + proxy := proxies[*params.GroupName] + if proxy == nil { + return false + } + log.Infoln("change proxy %s", proxy.Name()) + adapterProxy := proxy.(*adapter.Proxy) + selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + return false + } + if err := selector.Set(*params.ProxyName); err != nil { + return false + } + return true +} + +//export getTraffic +func getTraffic() *C.char { + up, down := statistic.DefaultManager.Now() + traffic := map[string]int64{ + "up": up, + "down": down, + } + data, err := json.Marshal(traffic) + if err != nil { + fmt.Println("Error:", err) + return C.CString("") + } + return C.CString(string(data)) +} + +//export asyncTestDelay +func asyncTestDelay(s *C.char) { + go func() { + paramsString := C.GoString(s) + var params = &TestDelayParams{} + err := json.Unmarshal([]byte(paramsString), params) + if err != nil { + return + } + + expectedStatus, err := utils.NewUnsignedRanges[uint16]("") + if err != nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout)) + defer cancel() + + proxies := tunnel.Proxies() + proxy := proxies[params.ProxyName] + + delayData := &Delay{ + Name: params.ProxyName, + } + + message := bridge.Message{ + Type: bridge.Delay, + Data: delayData, + } + + if proxy == nil { + delayData.Value = -1 + bridge.SendMessage(message) + return + } + + delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) + if err != nil || delay == 0 { + delayData.Value = -1 + bridge.SendMessage(message) + return + } + + delayData.Value = int32(delay) + bridge.SendMessage(message) + }() +} + +//export getVersionInfo +func getVersionInfo() *C.char { + versionInfo := map[string]string{ + "clashName": constant.Name, + "version": constant.Version, + } + data, err := json.Marshal(versionInfo) + if err != nil { + fmt.Println("Error:", err) + return C.CString("") + } + return C.CString(string(data)) +} + +//export getConnections +func getConnections() *C.char { + snapshot := statistic.DefaultManager.Snapshot() + data, err := json.Marshal(snapshot) + if err != nil { + fmt.Println("Error:", err) + return C.CString("") + } + return C.CString(string(data)) +} + +//export closeConnections +func closeConnections() bool { + statistic.DefaultManager.Range(func(c statistic.Tracker) bool { + err := c.Close() + if err != nil { + return false + } + return true + }) + return true +} + +//export closeConnection +func closeConnection(id *C.char) bool { + connectionId := C.GoString(id) + + err := statistic.DefaultManager.Get(connectionId).Close() + if err != nil { + return false + } + return true +} + +//export getProviders +func getProviders() *C.char { + data, err := json.Marshal(tunnel.Providers()) + if err != nil { + return C.CString("") + } + return C.CString(string(data)) +} + +//export getProvider +func getProvider(name *C.char) *C.char { + providerName := C.GoString(name) + providers := tunnel.Providers() + var provider = providers[providerName] + data, err := json.Marshal(provider) + if err != nil { + return C.CString("") + } + return C.CString(string(data)) +} + +//export initNativeApiBridge +func initNativeApiBridge(api unsafe.Pointer, port C.longlong) { + bridge.InitDartApi(api) + i := int64(port) + bridge.Port = &i +} diff --git a/core/log.go b/core/log.go new file mode 100644 index 0000000..d9846f9 --- /dev/null +++ b/core/log.go @@ -0,0 +1,36 @@ +package main + +import "C" +import ( + bridge "core/dart-bridge" + "github.com/metacubex/mihomo/common/observable" + "github.com/metacubex/mihomo/log" +) + +var logSubscriber observable.Subscription[log.Event] + +//export startLog +func startLog() { + if logSubscriber != nil { + log.UnSubscribe(logSubscriber) + logSubscriber = nil + } + logSubscriber = log.Subscribe() + go func() { + for logData := range logSubscriber { + message := &bridge.Message{ + Type: bridge.Log, + Data: logData, + } + bridge.SendMessage(*message) + } + }() +} + +//export stopLog +func stopLog() { + if logSubscriber != nil { + log.UnSubscribe(logSubscriber) + logSubscriber = nil + } +} diff --git a/core/main.go b/core/main.go new file mode 100644 index 0000000..c21ec05 --- /dev/null +++ b/core/main.go @@ -0,0 +1,10 @@ +package main + +import "C" +import ( + "fmt" +) + +func main() { + fmt.Println("init clash") +} diff --git a/core/process.go b/core/process.go new file mode 100644 index 0000000..487f7a0 --- /dev/null +++ b/core/process.go @@ -0,0 +1,3 @@ +//go:build android + +package main diff --git a/core/tun.go b/core/tun.go new file mode 100644 index 0000000..eae7d90 --- /dev/null +++ b/core/tun.go @@ -0,0 +1,83 @@ +//go:build android + +package main + +import "C" +import ( + t "core/tun" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/log" + "golang.org/x/sync/semaphore" + "sync" + "syscall" + "time" +) + +var tunLock sync.Mutex +var tun *t.Tun + +//export startTUN +func startTUN(fd C.int) bool { + tunLock.Lock() + defer tunLock.Unlock() + + if tun != nil { + tun.Close() + tun = nil + } + f := int(fd) + gateway := "172.16.0.1/30" + portal := "172.16.0.2" + dns := "0.0.0.0" + + tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)} + + closer, err := t.Start(f, gateway, portal, dns) + + applyConfig(true) + + if err != nil { + log.Errorln("startTUN error: %v", err) + tempTun.Close() + return false + } + + tempTun.Closer = closer + + tun = tempTun + + return true +} + +//export updateMarkSocketPort +func updateMarkSocketPort(markSocketPort C.longlong) bool { + tunLock.Lock() + defer tunLock.Unlock() + //if tun != nil { + // tun.MarkSocketPort = int64(markSocketPort) + //} + return true +} + +//export stopTun +func stopTun() { + tunLock.Lock() + defer tunLock.Unlock() + + if tun != nil { + tun.Close() + applyConfig(true) + tun = nil + } +} + +func init() { + dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { + return conn.Control(func(fd uintptr) { + if tun != nil { + tun.MarkSocket(int(fd)) + time.Sleep(time.Millisecond * 100) + } + }) + } +} diff --git a/core/tun/dns.go b/core/tun/dns.go new file mode 100644 index 0000000..2210ce4 --- /dev/null +++ b/core/tun/dns.go @@ -0,0 +1,33 @@ +//go:build android + +package tun + +import ( + "github.com/metacubex/mihomo/dns" + D "github.com/miekg/dns" + "net" +) + +func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool { + if targetPort != 53 { + return false + } + + return net.IPv4zero.Equal(dns) || target.Equal(dns) +} + +func relayDns(payload []byte) ([]byte, error) { + msg := &D.Msg{} + if err := msg.Unpack(payload); err != nil { + return nil, err + } + + r, err := dns.ServeDNSWithDefaultServer(msg) + if err != nil { + return nil, err + } + + r.SetRcode(msg, r.Rcode) + + return r.Pack() +} diff --git a/core/tun/tcp.go b/core/tun/tcp.go new file mode 100644 index 0000000..b8bd143 --- /dev/null +++ b/core/tun/tcp.go @@ -0,0 +1,20 @@ +//go:build android + +package tun + +import ( + "github.com/metacubex/mihomo/constant" + "net" +) + +func createMetadata(lAddr, rAddr *net.TCPAddr) *constant.Metadata { + return &constant.Metadata{ + NetWork: constant.TCP, + Type: constant.SOCKS5, + SrcIP: lAddr.AddrPort().Addr(), + DstIP: rAddr.AddrPort().Addr(), + SrcPort: uint16(lAddr.Port), + DstPort: uint16(rAddr.Port), + Host: "", + } +} diff --git a/core/tun/tun.go b/core/tun/tun.go new file mode 100644 index 0000000..8a57f1d --- /dev/null +++ b/core/tun/tun.go @@ -0,0 +1,204 @@ +//go:build android + +package tun + +import "C" +import ( + "context" + bridge "core/dart-bridge" + "encoding/binary" + "github.com/Kr328/tun2socket" + "github.com/Kr328/tun2socket/nat" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/socks5" + "github.com/metacubex/mihomo/tunnel" + "golang.org/x/sync/semaphore" + "io" + "net" + "os" + "strconv" + "time" +) + +type Tun struct { + Closer io.Closer + + Closed bool + Limit *semaphore.Weighted +} + +func (t *Tun) Close() { + _ = t.Limit.Acquire(context.TODO(), 4) + defer t.Limit.Release(4) + + t.Closed = true + + if t.Closer != nil { + _ = t.Closer.Close() + } +} + +var _, ipv4LoopBack, _ = net.ParseCIDR("127.0.0.0/8") + +func Start(fd int, gateway, portal, dns string) (io.Closer, error) { + device := os.NewFile(uintptr(fd), "/dev/tun") + ip, network, err := net.ParseCIDR(gateway) + if err != nil { + panic(err.Error()) + } else { + network.IP = ip + } + stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal)) + + if err != nil { + _ = device.Close() + return nil, err + } + + dnsAddr := net.ParseIP(dns) + + tcp := func() { + defer func(tcp *nat.TCP) { + _ = tcp.Close() + }(stack.TCP()) + defer log.Debugln("TCP: closed") + + for stack.TCP().SetDeadline(time.Time{}) == nil { + conn, err := stack.TCP().Accept() + if err != nil { + log.Errorln("Accept connection: %v", err) + continue + } + lAddr := conn.LocalAddr().(*net.TCPAddr) + rAddr := conn.RemoteAddr().(*net.TCPAddr) + + if ipv4LoopBack.Contains(rAddr.IP) { + _ = conn.Close() + + continue + } + + if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) { + go func() { + defer func(conn net.Conn) { + _ = conn.Close() + }(conn) + + buf := pool.Get(pool.UDPBufferSize) + defer func(buf []byte) { + _ = pool.Put(buf) + }(buf) + + for { + _ = conn.SetReadDeadline(time.Now().Add(constant.DefaultTCPTimeout)) + + length := uint16(0) + if err := binary.Read(conn, binary.BigEndian, &length); err != nil { + return + } + + if int(length) > len(buf) { + return + } + + n, err := conn.Read(buf[:length]) + if err != nil { + return + } + + msg, err := relayDns(buf[:n]) + if err != nil { + return + } + + _, _ = conn.Write(msg) + } + }() + + continue + } + + go tunnel.Tunnel.HandleTCPConn(conn, createMetadata(lAddr, rAddr)) + } + } + + udp := func() { + defer func(udp *nat.UDP) { + _ = udp.Close() + }(stack.UDP()) + defer log.Debugln("UDP: closed") + + for { + buf := pool.Get(pool.UDPBufferSize) + + n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf) + if err != nil { + return + } + + raw := buf[:n] + lAddr := lRAddr.(*net.UDPAddr) + rAddr := rRAddr.(*net.UDPAddr) + + if ipv4LoopBack.Contains(rAddr.IP) { + _ = pool.Put(buf) + + continue + } + + if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) { + go func() { + defer func(buf []byte) { + _ = pool.Put(buf) + }(buf) + + msg, err := relayDns(raw) + if err != nil { + return + } + + _, _ = stack.UDP().WriteTo(msg, rAddr, lAddr) + }() + + continue + } + + pkt := &packet{ + local: lAddr, + data: raw, + writeBack: func(b []byte, addr net.Addr) (int, error) { + return stack.UDP().WriteTo(b, addr, lAddr) + }, + drop: func() { + _ = pool.Put(buf) + }, + } + + tunnel.Tunnel.HandleUDPPacket(inbound.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, constant.SOCKS5)) + } + } + + go tcp() + go udp() + + return stack, nil +} + +func (t *Tun) MarkSocket(fd int) { + _ = t.Limit.Acquire(context.Background(), 1) + defer t.Limit.Release(1) + + if t.Closed { + return + } + + message := &bridge.Message{ + Type: bridge.Tun, + Data: strconv.Itoa(fd), + } + + bridge.SendMessage(*message) +} diff --git a/core/tun/udp.go b/core/tun/udp.go new file mode 100644 index 0000000..c491f24 --- /dev/null +++ b/core/tun/udp.go @@ -0,0 +1,28 @@ +//go:build android + +package tun + +import "net" + +type packet struct { + local *net.UDPAddr + data []byte + writeBack func(b []byte, addr net.Addr) (int, error) + drop func() +} + +func (pkt *packet) Data() []byte { + return pkt.data +} + +func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + return pkt.writeBack(b, addr) +} + +func (pkt *packet) Drop() { + pkt.drop() +} + +func (pkt *packet) LocalAddr() net.Addr { + return pkt.local +} diff --git a/distribute_options.yaml b/distribute_options.yaml new file mode 100644 index 0000000..82c1ea7 --- /dev/null +++ b/distribute_options.yaml @@ -0,0 +1,2 @@ +app_name: 'FlClash' +output: 'dist/' diff --git a/lib/application.dart b/lib/application.dart new file mode 100644 index 0000000..e1f6e54 --- /dev/null +++ b/lib/application.dart @@ -0,0 +1,166 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:fl_clash/l10n/l10n.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; + +import 'controller.dart'; +import 'models/models.dart'; +import 'pages/pages.dart'; + +runAppWithPreferences( + Widget child, { + required AppState appState, + required Config config, + required ClashConfig clashConfig, +}) { + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => clashConfig, + ), + ChangeNotifierProvider( + create: (_) => config, + ), + ChangeNotifierProvider( + create: (_) => appState, + ) + ], + child: child, + )); +} + +class Application extends StatefulWidget { + const Application({ + super.key, + }); + + @override + State createState() => ApplicationState(); +} + +class ApplicationState extends State { + late AppController appController; + late SystemColorSchemes systemColorSchemes; + + ColorScheme _getAppColorScheme({ + required Brightness brightness, + int? primaryColor, + required SystemColorSchemes systemColorSchemes, + }) { + if (primaryColor != null) { + return ColorScheme.fromSeed( + seedColor: Color(primaryColor), + brightness: brightness, + ); + } else { + return systemColorSchemes.getSystemColorSchemeForBrightness(brightness); + } + } + + @override + void initState() { + super.initState(); + appController = AppController(context); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + appController.afterInit(); + appController.initLink(); + }); + } + + _buildApp(Widget app) { + if (system.isDesktop) { + return WindowContainer( + child: TrayContainer( + child: app, + ), + ); + } + return AndroidContainer( + child: TileContainer( + child: app, + ), + ); + } + + _updateSystemColorSchemes( + ColorScheme? lightDynamic, + ColorScheme? darkDynamic, + ) { + systemColorSchemes = SystemColorSchemes( + lightColorScheme: lightDynamic, + darkColorScheme: darkDynamic, + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + appController.updateSystemColorSchemes(systemColorSchemes); + }); + } + + @override + Widget build(context) { + return AppStateContainer( + child: ClashMessageContainer( + child: _buildApp( + Selector2( + selector: (_, appState, config) => ApplicationSelectorState( + locale: config.locale, + themeMode: config.themeMode, + primaryColor: config.primaryColor, + ), + builder: (_, state, child) { + debugPrint("[Application] update===>"); + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + _updateSystemColorSchemes(lightDynamic, darkDynamic); + return MaterialApp( + navigatorKey: globalState.navigatorKey, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate + ], + title: appConstant.name, + locale: Other.getLocaleForString(state.locale), + supportedLocales: + AppLocalizations.delegate.supportedLocales, + themeMode: state.themeMode, + theme: ThemeData( + useMaterial3: true, + colorScheme: _getAppColorScheme( + brightness: Brightness.light, + systemColorSchemes: systemColorSchemes, + primaryColor: state.primaryColor, + ), + ), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: _getAppColorScheme( + brightness: Brightness.dark, + systemColorSchemes: systemColorSchemes, + primaryColor: state.primaryColor, + ), + ), + home: child, + ); + }, + ); + }, + child: const HomePage(), + ), + ), + ), + ); + } + + @override + Future dispose() async { + linkManager.destroy(); + await appController.savePreferences(); + super.dispose(); + } +} diff --git a/lib/clash/clash.dart b/lib/clash/clash.dart new file mode 100644 index 0000000..b7a61a3 --- /dev/null +++ b/lib/clash/clash.dart @@ -0,0 +1,3 @@ +export 'core.dart'; +export 'service.dart'; +export 'message.dart'; \ No newline at end of file diff --git a/lib/clash/core.dart b/lib/clash/core.dart new file mode 100644 index 0000000..cccda16 --- /dev/null +++ b/lib/clash/core.dart @@ -0,0 +1,162 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:ffi/ffi.dart'; +import '../enum/enum.dart'; +import '../models/models.dart'; +import '../common/common.dart'; +import 'generated/clash_ffi.dart'; + +class ClashCore { + static ClashCore? _instance; + static final receiver = ReceivePort(); + + late final ClashFFI clashFFI; + late final DynamicLibrary lib; + + ClashCore._internal() { + if (Platform.isWindows) { + lib = DynamicLibrary.open("libclash.dll"); + clashFFI = ClashFFI(lib); + } + if (Platform.isMacOS) { + lib = DynamicLibrary.open("libclash.dylib"); + clashFFI = ClashFFI(lib); + } + if (Platform.isAndroid || Platform.isLinux) { + lib = DynamicLibrary.open("libclash.so"); + clashFFI = ClashFFI(lib); + } + clashFFI.initNativeApiBridge( + NativeApi.initializeApiDLData, + receiver.sendPort.nativePort, + ); + } + + factory ClashCore() { + _instance ??= ClashCore._internal(); + return _instance!; + } + + bool init(String homeDir) { + return clashFFI.initClash( + homeDir.toNativeUtf8().cast(), + ) == + 1; + } + + shutdown() { + clashFFI.shutdownClash(); + lib.close(); + } + + bool get isInit => clashFFI.getIsInit() == 1; + + bool validateConfig(String data) { + return clashFFI.validateConfig( + data.toNativeUtf8().cast(), + ) == + 1; + } + + bool updateConfig(UpdateConfigParams updateConfigParams) { + final params = json.encode(updateConfigParams); + return clashFFI.updateConfig( + params.toNativeUtf8().cast(), + ) == + 1; + } + + List getProxiesGroups() { + final proxiesRaw = clashFFI.getProxies(); + final proxies = json.decode(proxiesRaw.cast().toDartString()); + final groupsRaw = List.from(proxies.values).where((e) { + final excludeName = !UsedProxyExtension.valueList + .where((element) => element != UsedProxy.GLOBAL.name) + .contains(e['name']); + final validType = GroupTypeExtension.valueList.contains(e['type']); + return excludeName && validType; + }).map( + (e) { + e["all"] = ((e["all"] ?? []) as List) + .map( + (name) => proxies[name], + ) + .toList(); + return e; + }, + ).toList() + ..sort( + (a, b) { + final aIndex = GroupTypeExtension.getGroupType(a['type'])?.index; + final bIndex = GroupTypeExtension.getGroupType(b['type'])?.index; + if (a == null && b == null) { + return 0; + } + if (a == null) { + return 1; + } + if (b == null) { + return -1; + } + return aIndex! - bIndex!; + }, + ); + final groups = groupsRaw.map((e) => Group.fromJson(e)).toList(); + return groups; + } + + bool changeProxy(ChangeProxyParams changeProxyParams) { + final params = json.encode(changeProxyParams); + return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1; + } + + bool delay(String proxyName) { + final delayParams = { + "proxy-name": proxyName, + "timeout": appConstant.httpTimeoutDuration.inMilliseconds, + }; + clashFFI.asyncTestDelay(json.encode(delayParams).toNativeUtf8().cast()); + return true; + } + + VersionInfo getVersionInfo() { + final versionInfoRaw = clashFFI.getVersionInfo(); + final versionInfo = json.decode(versionInfoRaw.cast().toDartString()); + return VersionInfo.fromJson(versionInfo); + } + + Traffic getTraffic() { + final trafficRaw = clashFFI.getTraffic(); + final trafficMap = json.decode(trafficRaw.cast().toDartString()); + return Traffic.fromMap(trafficMap); + } + + void startLog() { + clashFFI.startLog(); + } + + stopLog() { + clashFFI.stopLog(); + } + + startTun(int fd) { + clashFFI.startTUN(fd); + } + + void stopTun() { + clashFFI.stopTun(); + } + + List getConnections() { + final connectionsDataRaw = clashFFI.getConnections(); + final connectionsData = + json.decode(connectionsDataRaw.cast().toDartString()) as Map; + final connectionsRaw = connectionsData['connections'] as List? ?? []; + return connectionsRaw.map((e) => Connection.fromJson(e)).toList(); + } +} + +final clashCore = ClashCore(); diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart new file mode 100644 index 0000000..c01c6cc --- /dev/null +++ b/lib/clash/generated/clash_ffi.dart @@ -0,0 +1,1259 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +class ClashFFI { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + ClashFFI(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + ClashFFI.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + void __va_start( + ffi.Pointer arg0, + ) { + return ___va_start( + arg0, + ); + } + + late final ___va_startPtr = + _lookup)>>( + '__va_start'); + late final ___va_start = + ___va_startPtr.asFunction)>(); + + void __security_init_cookie() { + return ___security_init_cookie(); + } + + late final ___security_init_cookiePtr = + _lookup>( + '__security_init_cookie'); + late final ___security_init_cookie = + ___security_init_cookiePtr.asFunction(); + + void __security_check_cookie( + int _StackCookie, + ) { + return ___security_check_cookie( + _StackCookie, + ); + } + + late final ___security_check_cookiePtr = + _lookup>( + '__security_check_cookie'); + late final ___security_check_cookie = + ___security_check_cookiePtr.asFunction(); + + void __report_gsfailure( + int _StackCookie, + ) { + return ___report_gsfailure( + _StackCookie, + ); + } + + late final ___report_gsfailurePtr = + _lookup>( + '__report_gsfailure'); + late final ___report_gsfailure = + ___report_gsfailurePtr.asFunction(); + + late final ffi.Pointer ___security_cookie = + _lookup('__security_cookie'); + + int get __security_cookie => ___security_cookie.value; + + set __security_cookie(int value) => ___security_cookie.value = value; + + void _invalid_parameter_noinfo() { + return __invalid_parameter_noinfo(); + } + + late final __invalid_parameter_noinfoPtr = + _lookup>( + '_invalid_parameter_noinfo'); + late final __invalid_parameter_noinfo = + __invalid_parameter_noinfoPtr.asFunction(); + + void _invalid_parameter_noinfo_noreturn() { + return __invalid_parameter_noinfo_noreturn(); + } + + late final __invalid_parameter_noinfo_noreturnPtr = + _lookup>( + '_invalid_parameter_noinfo_noreturn'); + late final __invalid_parameter_noinfo_noreturn = + __invalid_parameter_noinfo_noreturnPtr.asFunction(); + + void _invoke_watson( + ffi.Pointer _Expression, + ffi.Pointer _FunctionName, + ffi.Pointer _FileName, + int _LineNo, + int _Reserved, + ) { + return __invoke_watson( + _Expression, + _FunctionName, + _FileName, + _LineNo, + _Reserved, + ); + } + + late final __invoke_watsonPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.UnsignedInt, + ffi.UintPtr)>>('_invoke_watson'); + late final __invoke_watson = __invoke_watsonPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, int, int)>(); + + ffi.Pointer _errno() { + return __errno(); + } + + late final __errnoPtr = + _lookup Function()>>('_errno'); + late final __errno = __errnoPtr.asFunction Function()>(); + + int _set_errno( + int _Value, + ) { + return __set_errno( + _Value, + ); + } + + late final __set_errnoPtr = + _lookup>('_set_errno'); + late final __set_errno = __set_errnoPtr.asFunction(); + + int _get_errno( + ffi.Pointer _Value, + ) { + return __get_errno( + _Value, + ); + } + + late final __get_errnoPtr = + _lookup)>>( + '_get_errno'); + late final __get_errno = + __get_errnoPtr.asFunction)>(); + + int __threadid() { + return ___threadid(); + } + + late final ___threadidPtr = + _lookup>('__threadid'); + late final ___threadid = ___threadidPtr.asFunction(); + + int __threadhandle() { + return ___threadhandle(); + } + + late final ___threadhandlePtr = + _lookup>('__threadhandle'); + late final ___threadhandle = ___threadhandlePtr.asFunction(); + + double cabs( + _Dcomplex _Z, + ) { + return _cabs( + _Z, + ); + } + + late final _cabsPtr = + _lookup>('cabs'); + late final _cabs = _cabsPtr.asFunction(); + + _Dcomplex cacos( + _Dcomplex _Z, + ) { + return _cacos( + _Z, + ); + } + + late final _cacosPtr = + _lookup>('cacos'); + late final _cacos = _cacosPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex cacosh( + _Dcomplex _Z, + ) { + return _cacosh( + _Z, + ); + } + + late final _cacoshPtr = + _lookup>('cacosh'); + late final _cacosh = _cacoshPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + double carg( + _Dcomplex _Z, + ) { + return _carg( + _Z, + ); + } + + late final _cargPtr = + _lookup>('carg'); + late final _carg = _cargPtr.asFunction(); + + _Dcomplex casin( + _Dcomplex _Z, + ) { + return _casin( + _Z, + ); + } + + late final _casinPtr = + _lookup>('casin'); + late final _casin = _casinPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex casinh( + _Dcomplex _Z, + ) { + return _casinh( + _Z, + ); + } + + late final _casinhPtr = + _lookup>('casinh'); + late final _casinh = _casinhPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex catan( + _Dcomplex _Z, + ) { + return _catan( + _Z, + ); + } + + late final _catanPtr = + _lookup>('catan'); + late final _catan = _catanPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex catanh( + _Dcomplex _Z, + ) { + return _catanh( + _Z, + ); + } + + late final _catanhPtr = + _lookup>('catanh'); + late final _catanh = _catanhPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex ccos( + _Dcomplex _Z, + ) { + return _ccos( + _Z, + ); + } + + late final _ccosPtr = + _lookup>('ccos'); + late final _ccos = _ccosPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex ccosh( + _Dcomplex _Z, + ) { + return _ccosh( + _Z, + ); + } + + late final _ccoshPtr = + _lookup>('ccosh'); + late final _ccosh = _ccoshPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex cexp( + _Dcomplex _Z, + ) { + return _cexp( + _Z, + ); + } + + late final _cexpPtr = + _lookup>('cexp'); + late final _cexp = _cexpPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + double cimag( + _Dcomplex _Z, + ) { + return _cimag( + _Z, + ); + } + + late final _cimagPtr = + _lookup>('cimag'); + late final _cimag = _cimagPtr.asFunction(); + + _Dcomplex clog( + _Dcomplex _Z, + ) { + return _clog( + _Z, + ); + } + + late final _clogPtr = + _lookup>('clog'); + late final _clog = _clogPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex clog10( + _Dcomplex _Z, + ) { + return _clog10( + _Z, + ); + } + + late final _clog10Ptr = + _lookup>('clog10'); + late final _clog10 = _clog10Ptr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex conj( + _Dcomplex _Z, + ) { + return _conj( + _Z, + ); + } + + late final _conjPtr = + _lookup>('conj'); + late final _conj = _conjPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex cpow( + _Dcomplex _X, + _Dcomplex _Y, + ) { + return _cpow( + _X, + _Y, + ); + } + + late final _cpowPtr = + _lookup>( + 'cpow'); + late final _cpow = + _cpowPtr.asFunction<_Dcomplex Function(_Dcomplex, _Dcomplex)>(); + + _Dcomplex cproj( + _Dcomplex _Z, + ) { + return _cproj( + _Z, + ); + } + + late final _cprojPtr = + _lookup>('cproj'); + late final _cproj = _cprojPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + double creal( + _Dcomplex _Z, + ) { + return _creal( + _Z, + ); + } + + late final _crealPtr = + _lookup>('creal'); + late final _creal = _crealPtr.asFunction(); + + _Dcomplex csin( + _Dcomplex _Z, + ) { + return _csin( + _Z, + ); + } + + late final _csinPtr = + _lookup>('csin'); + late final _csin = _csinPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex csinh( + _Dcomplex _Z, + ) { + return _csinh( + _Z, + ); + } + + late final _csinhPtr = + _lookup>('csinh'); + late final _csinh = _csinhPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex csqrt( + _Dcomplex _Z, + ) { + return _csqrt( + _Z, + ); + } + + late final _csqrtPtr = + _lookup>('csqrt'); + late final _csqrt = _csqrtPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex ctan( + _Dcomplex _Z, + ) { + return _ctan( + _Z, + ); + } + + late final _ctanPtr = + _lookup>('ctan'); + late final _ctan = _ctanPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + _Dcomplex ctanh( + _Dcomplex _Z, + ) { + return _ctanh( + _Z, + ); + } + + late final _ctanhPtr = + _lookup>('ctanh'); + late final _ctanh = _ctanhPtr.asFunction<_Dcomplex Function(_Dcomplex)>(); + + double norm( + _Dcomplex _Z, + ) { + return _norm( + _Z, + ); + } + + late final _normPtr = + _lookup>('norm'); + late final _norm = _normPtr.asFunction(); + + double cabsf( + _Fcomplex _Z, + ) { + return _cabsf( + _Z, + ); + } + + late final _cabsfPtr = + _lookup>('cabsf'); + late final _cabsf = _cabsfPtr.asFunction(); + + _Fcomplex cacosf( + _Fcomplex _Z, + ) { + return _cacosf( + _Z, + ); + } + + late final _cacosfPtr = + _lookup>('cacosf'); + late final _cacosf = _cacosfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex cacoshf( + _Fcomplex _Z, + ) { + return _cacoshf( + _Z, + ); + } + + late final _cacoshfPtr = + _lookup>('cacoshf'); + late final _cacoshf = _cacoshfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + double cargf( + _Fcomplex _Z, + ) { + return _cargf( + _Z, + ); + } + + late final _cargfPtr = + _lookup>('cargf'); + late final _cargf = _cargfPtr.asFunction(); + + _Fcomplex casinf( + _Fcomplex _Z, + ) { + return _casinf( + _Z, + ); + } + + late final _casinfPtr = + _lookup>('casinf'); + late final _casinf = _casinfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex casinhf( + _Fcomplex _Z, + ) { + return _casinhf( + _Z, + ); + } + + late final _casinhfPtr = + _lookup>('casinhf'); + late final _casinhf = _casinhfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex catanf( + _Fcomplex _Z, + ) { + return _catanf( + _Z, + ); + } + + late final _catanfPtr = + _lookup>('catanf'); + late final _catanf = _catanfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex catanhf( + _Fcomplex _Z, + ) { + return _catanhf( + _Z, + ); + } + + late final _catanhfPtr = + _lookup>('catanhf'); + late final _catanhf = _catanhfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex ccosf( + _Fcomplex _Z, + ) { + return _ccosf( + _Z, + ); + } + + late final _ccosfPtr = + _lookup>('ccosf'); + late final _ccosf = _ccosfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex ccoshf( + _Fcomplex _Z, + ) { + return _ccoshf( + _Z, + ); + } + + late final _ccoshfPtr = + _lookup>('ccoshf'); + late final _ccoshf = _ccoshfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex cexpf( + _Fcomplex _Z, + ) { + return _cexpf( + _Z, + ); + } + + late final _cexpfPtr = + _lookup>('cexpf'); + late final _cexpf = _cexpfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + double cimagf( + _Fcomplex _Z, + ) { + return _cimagf( + _Z, + ); + } + + late final _cimagfPtr = + _lookup>('cimagf'); + late final _cimagf = _cimagfPtr.asFunction(); + + _Fcomplex clogf( + _Fcomplex _Z, + ) { + return _clogf( + _Z, + ); + } + + late final _clogfPtr = + _lookup>('clogf'); + late final _clogf = _clogfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex clog10f( + _Fcomplex _Z, + ) { + return _clog10f( + _Z, + ); + } + + late final _clog10fPtr = + _lookup>('clog10f'); + late final _clog10f = _clog10fPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex conjf( + _Fcomplex _Z, + ) { + return _conjf( + _Z, + ); + } + + late final _conjfPtr = + _lookup>('conjf'); + late final _conjf = _conjfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex cpowf( + _Fcomplex _X, + _Fcomplex _Y, + ) { + return _cpowf( + _X, + _Y, + ); + } + + late final _cpowfPtr = + _lookup>( + 'cpowf'); + late final _cpowf = + _cpowfPtr.asFunction<_Fcomplex Function(_Fcomplex, _Fcomplex)>(); + + _Fcomplex cprojf( + _Fcomplex _Z, + ) { + return _cprojf( + _Z, + ); + } + + late final _cprojfPtr = + _lookup>('cprojf'); + late final _cprojf = _cprojfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + double crealf( + _Fcomplex _Z, + ) { + return _crealf( + _Z, + ); + } + + late final _crealfPtr = + _lookup>('crealf'); + late final _crealf = _crealfPtr.asFunction(); + + _Fcomplex csinf( + _Fcomplex _Z, + ) { + return _csinf( + _Z, + ); + } + + late final _csinfPtr = + _lookup>('csinf'); + late final _csinf = _csinfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex csinhf( + _Fcomplex _Z, + ) { + return _csinhf( + _Z, + ); + } + + late final _csinhfPtr = + _lookup>('csinhf'); + late final _csinhf = _csinhfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex csqrtf( + _Fcomplex _Z, + ) { + return _csqrtf( + _Z, + ); + } + + late final _csqrtfPtr = + _lookup>('csqrtf'); + late final _csqrtf = _csqrtfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex ctanf( + _Fcomplex _Z, + ) { + return _ctanf( + _Z, + ); + } + + late final _ctanfPtr = + _lookup>('ctanf'); + late final _ctanf = _ctanfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + _Fcomplex ctanhf( + _Fcomplex _Z, + ) { + return _ctanhf( + _Z, + ); + } + + late final _ctanhfPtr = + _lookup>('ctanhf'); + late final _ctanhf = _ctanhfPtr.asFunction<_Fcomplex Function(_Fcomplex)>(); + + double normf( + _Fcomplex _Z, + ) { + return _normf( + _Z, + ); + } + + late final _normfPtr = + _lookup>('normf'); + late final _normf = _normfPtr.asFunction(); + + _Dcomplex _Cbuild( + double _Re, + double _Im, + ) { + return __Cbuild( + _Re, + _Im, + ); + } + + late final __CbuildPtr = + _lookup>( + '_Cbuild'); + late final __Cbuild = + __CbuildPtr.asFunction<_Dcomplex Function(double, double)>(); + + _Dcomplex _Cmulcc( + _Dcomplex _X, + _Dcomplex _Y, + ) { + return __Cmulcc( + _X, + _Y, + ); + } + + late final __CmulccPtr = + _lookup>( + '_Cmulcc'); + late final __Cmulcc = + __CmulccPtr.asFunction<_Dcomplex Function(_Dcomplex, _Dcomplex)>(); + + _Dcomplex _Cmulcr( + _Dcomplex _X, + double _Y, + ) { + return __Cmulcr( + _X, + _Y, + ); + } + + late final __CmulcrPtr = + _lookup>( + '_Cmulcr'); + late final __Cmulcr = + __CmulcrPtr.asFunction<_Dcomplex Function(_Dcomplex, double)>(); + + _Fcomplex _FCbuild( + double _Re, + double _Im, + ) { + return __FCbuild( + _Re, + _Im, + ); + } + + late final __FCbuildPtr = + _lookup>( + '_FCbuild'); + late final __FCbuild = + __FCbuildPtr.asFunction<_Fcomplex Function(double, double)>(); + + _Fcomplex _FCmulcc( + _Fcomplex _X, + _Fcomplex _Y, + ) { + return __FCmulcc( + _X, + _Y, + ); + } + + late final __FCmulccPtr = + _lookup>( + '_FCmulcc'); + late final __FCmulcc = + __FCmulccPtr.asFunction<_Fcomplex Function(_Fcomplex, _Fcomplex)>(); + + _Fcomplex _FCmulcr( + _Fcomplex _X, + double _Y, + ) { + return __FCmulcr( + _X, + _Y, + ); + } + + late final __FCmulcrPtr = + _lookup>( + '_FCmulcr'); + late final __FCmulcr = + __FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>(); + + int initClash( + ffi.Pointer homeDirStr, + ) { + return _initClash( + homeDirStr, + ); + } + + late final _initClashPtr = + _lookup)>>( + 'initClash'); + late final _initClash = + _initClashPtr.asFunction)>(); + + int getIsInit() { + return _getIsInit(); + } + + late final _getIsInitPtr = + _lookup>('getIsInit'); + late final _getIsInit = _getIsInitPtr.asFunction(); + + int restartClash() { + return _restartClash(); + } + + late final _restartClashPtr = + _lookup>('restartClash'); + late final _restartClash = _restartClashPtr.asFunction(); + + int shutdownClash() { + return _shutdownClash(); + } + + late final _shutdownClashPtr = + _lookup>('shutdownClash'); + late final _shutdownClash = _shutdownClashPtr.asFunction(); + + int validateConfig( + ffi.Pointer s, + ) { + return _validateConfig( + s, + ); + } + + late final _validateConfigPtr = + _lookup)>>( + 'validateConfig'); + late final _validateConfig = + _validateConfigPtr.asFunction)>(); + + int updateConfig( + ffi.Pointer s, + ) { + return _updateConfig( + s, + ); + } + + late final _updateConfigPtr = + _lookup)>>( + 'updateConfig'); + late final _updateConfig = + _updateConfigPtr.asFunction)>(); + + ffi.Pointer getProxies() { + return _getProxies(); + } + + late final _getProxiesPtr = + _lookup Function()>>( + 'getProxies'); + late final _getProxies = + _getProxiesPtr.asFunction Function()>(); + + int changeProxy( + ffi.Pointer s, + ) { + return _changeProxy( + s, + ); + } + + late final _changeProxyPtr = + _lookup)>>( + 'changeProxy'); + late final _changeProxy = + _changeProxyPtr.asFunction)>(); + + ffi.Pointer getTraffic() { + return _getTraffic(); + } + + late final _getTrafficPtr = + _lookup Function()>>( + 'getTraffic'); + late final _getTraffic = + _getTrafficPtr.asFunction Function()>(); + + void asyncTestDelay( + ffi.Pointer s, + ) { + return _asyncTestDelay( + s, + ); + } + + late final _asyncTestDelayPtr = + _lookup)>>( + 'asyncTestDelay'); + late final _asyncTestDelay = + _asyncTestDelayPtr.asFunction)>(); + + ffi.Pointer getVersionInfo() { + return _getVersionInfo(); + } + + late final _getVersionInfoPtr = + _lookup Function()>>( + 'getVersionInfo'); + late final _getVersionInfo = + _getVersionInfoPtr.asFunction Function()>(); + + ffi.Pointer getConnections() { + return _getConnections(); + } + + late final _getConnectionsPtr = + _lookup Function()>>( + 'getConnections'); + late final _getConnections = + _getConnectionsPtr.asFunction Function()>(); + + int closeConnections() { + return _closeConnections(); + } + + late final _closeConnectionsPtr = + _lookup>('closeConnections'); + late final _closeConnections = + _closeConnectionsPtr.asFunction(); + + int closeConnection( + ffi.Pointer id, + ) { + return _closeConnection( + id, + ); + } + + late final _closeConnectionPtr = + _lookup)>>( + 'closeConnection'); + late final _closeConnection = + _closeConnectionPtr.asFunction)>(); + + ffi.Pointer getProviders() { + return _getProviders(); + } + + late final _getProvidersPtr = + _lookup Function()>>( + 'getProviders'); + late final _getProviders = + _getProvidersPtr.asFunction Function()>(); + + ffi.Pointer getProvider( + ffi.Pointer name, + ) { + return _getProvider( + name, + ); + } + + late final _getProviderPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('getProvider'); + late final _getProvider = _getProviderPtr + .asFunction Function(ffi.Pointer)>(); + + void initNativeApiBridge( + ffi.Pointer api, + int port, + ) { + return _initNativeApiBridge( + api, + port, + ); + } + + late final _initNativeApiBridgePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.LongLong)>>('initNativeApiBridge'); + late final _initNativeApiBridge = _initNativeApiBridgePtr + .asFunction, int)>(); + + void startLog() { + return _startLog(); + } + + late final _startLogPtr = + _lookup>('startLog'); + late final _startLog = _startLogPtr.asFunction(); + + void stopLog() { + return _stopLog(); + } + + late final _stopLogPtr = + _lookup>('stopLog'); + late final _stopLog = _stopLogPtr.asFunction(); + + int startTUN( + int fd, + ) { + return _startTUN( + fd, + ); + } + + late final _startTUNPtr = + _lookup>('startTUN'); + late final _startTUN = _startTUNPtr.asFunction(); + + int updateMarkSocketPort( + int markSocketPort, + ) { + return _updateMarkSocketPort( + markSocketPort, + ); + } + + late final _updateMarkSocketPortPtr = + _lookup>( + 'updateMarkSocketPort'); + late final _updateMarkSocketPort = + _updateMarkSocketPortPtr.asFunction(); + + void stopTun() { + return _stopTun(); + } + + late final _stopTunPtr = + _lookup>('stopTun'); + late final _stopTun = _stopTunPtr.asFunction(); +} + +typedef va_list = ffi.Pointer; + +final class __crt_locale_data_public extends ffi.Struct { + external ffi.Pointer _locale_pctype; + + @ffi.Int() + external int _locale_mb_cur_max; + + @ffi.UnsignedInt() + external int _locale_lc_codepage; +} + +final class __crt_locale_pointers extends ffi.Struct { + external ffi.Pointer<__crt_locale_data> locinfo; + + external ffi.Pointer<__crt_multibyte_data> mbcinfo; +} + +final class __crt_locale_data extends ffi.Opaque {} + +final class __crt_multibyte_data extends ffi.Opaque {} + +final class _Mbstatet extends ffi.Struct { + @ffi.UnsignedLong() + external int _Wchar; + + @ffi.UnsignedShort() + external int _Byte; + + @ffi.UnsignedShort() + external int _State; +} + +typedef errno_t = ffi.Int; +typedef Darterrno_t = int; + +final class _GoString_ extends ffi.Struct { + external ffi.Pointer p; + + @ptrdiff_t() + external int n; +} + +typedef ptrdiff_t = ffi.LongLong; +typedef Dartptrdiff_t = int; + +final class _C_double_complex extends ffi.Struct { + @ffi.Array.multi([2]) + external ffi.Array _Val; +} + +final class _C_float_complex extends ffi.Struct { + @ffi.Array.multi([2]) + external ffi.Array _Val; +} + +final class _C_ldouble_complex extends ffi.Opaque {} + +typedef _Dcomplex = _C_double_complex; +typedef _Fcomplex = _C_float_complex; + +final class GoInterface extends ffi.Struct { + external ffi.Pointer t; + + external ffi.Pointer v; +} + +final class GoSlice extends ffi.Struct { + external ffi.Pointer data; + + @GoInt() + external int len; + + @GoInt() + external int cap; +} + +typedef GoInt = GoInt64; +typedef GoInt64 = ffi.LongLong; +typedef DartGoInt64 = int; +typedef GoUint8 = ffi.UnsignedChar; +typedef DartGoUint8 = int; + +const int _VCRT_COMPILER_PREPROCESSOR = 1; + +const int _SAL_VERSION = 20; + +const int __SAL_H_VERSION = 180000000; + +const int _USE_DECLSPECS_FOR_SAL = 0; + +const int _USE_ATTRIBUTES_FOR_SAL = 0; + +const int _CRT_PACKING = 8; + +const int _VCRUNTIME_DISABLED_WARNINGS = 4514; + +const int _HAS_EXCEPTIONS = 1; + +const int _WCHAR_T_DEFINED = 1; + +const int NULL = 0; + +const int _HAS_CXX17 = 0; + +const int _HAS_CXX20 = 0; + +const int _HAS_CXX23 = 0; + +const int _HAS_NODISCARD = 1; + +const int _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE = 1; + +const int _CRT_BUILD_DESKTOP_APP = 1; + +const int _UCRT_DISABLED_WARNINGS = 4324; + +const int _ARGMAX = 100; + +const int _TRUNCATE = -1; + +const int _CRT_INT_MAX = 2147483647; + +const int _CRT_SIZE_MAX = -1; + +const String __FILEW__ = 'C'; + +const int _CRT_FUNCTIONS_REQUIRED = 1; + +const int _CRT_HAS_CXX17 = 0; + +const int _CRT_HAS_C11 = 0; + +const int _CRT_INTERNAL_NONSTDC_NAMES = 1; + +const int __STDC_SECURE_LIB__ = 200411; + +const int __GOT_SECURE_LIB__ = 200411; + +const int __STDC_WANT_SECURE_LIB__ = 1; + +const int _SECURECRT_FILL_BUFFER_PATTERN = 254; + +const int _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES = 0; + +const int _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT = 0; + +const int _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES = 1; + +const int _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_MEMORY = 0; + +const int _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES_MEMORY = 0; diff --git a/lib/clash/message.dart b/lib/clash/message.dart new file mode 100644 index 0000000..8bb014b --- /dev/null +++ b/lib/clash/message.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/foundation.dart'; + +import 'core.dart'; + +abstract mixin class ClashMessageListener { + void onLog(Log log) {} + + void onTun(String fd) {} + + void onDelay(Delay delay) {} + + void onProcess(Metadata metadata) {} +} + +class ClashMessage { + StreamSubscription? subscription; + + ClashMessage._() { + if (subscription != null) { + subscription!.cancel(); + subscription = null; + } + subscription = ClashCore.receiver.listen((message) { + final m = Message.fromJson(json.decode(message)); + for (final ClashMessageListener listener in _listeners) { + switch (m.type) { + case MessageType.log: + listener.onLog(Log.fromJson(m.data)); + break; + case MessageType.tun: + listener.onTun(m.data); + break; + case MessageType.delay: + listener.onDelay(Delay.fromJson(m.data)); + break; + case MessageType.process: + listener.onProcess(Metadata.fromJson(m.data)); + break; + } + } + }); + } + + static final ClashMessage instance = ClashMessage._(); + + final ObserverList _listeners = + ObserverList(); + + bool get hasListeners { + return _listeners.isNotEmpty; + } + + void addListener(ClashMessageListener listener) { + _listeners.add(listener); + } + + void removeListener(ClashMessageListener listener) { + _listeners.remove(listener); + } +} + +final clashMessage = ClashMessage.instance; diff --git a/lib/clash/service.dart b/lib/clash/service.dart new file mode 100644 index 0000000..4d83d60 --- /dev/null +++ b/lib/clash/service.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/services.dart'; +import 'core.dart'; + +class ClashService { + Future initMmdb() async { + final mmdbPath = await appPath.getMMDBPath(); + var mmdbFile = File(mmdbPath); + final isExists = await mmdbFile.exists(); + if (isExists) return true; + try { + mmdbFile = await mmdbFile.create(recursive: true); + ByteData data = await rootBundle.load('assets/data/geoip.metadb'); + List bytes = data.buffer.asUint8List(); + await mmdbFile.writeAsBytes(bytes, flush: true); + return true; + } catch (_) { + return false; + } + } + + Future init({ + required ClashConfig clashConfig, + required Config config, + }) async { + final isInitMmdb = await initMmdb(); + if (!isInitMmdb) return false; + final homeDirPath = await appPath.getHomeDirPath(); + final isInit = clashCore.init(homeDirPath); + return isInit; + } +} + +final clashService = ClashService(); diff --git a/lib/common/android.dart b/lib/common/android.dart new file mode 100644 index 0000000..143403b --- /dev/null +++ b/lib/common/android.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/plugins/app.dart'; + +class Android { + init() async { + app?.onExit = () { + clashCore.shutdown(); + exit(0); + }; + } +} + +final android = Platform.isAndroid ? Android() : null; diff --git a/lib/common/app_localizations.dart b/lib/common/app_localizations.dart new file mode 100644 index 0000000..497e093 --- /dev/null +++ b/lib/common/app_localizations.dart @@ -0,0 +1,3 @@ +import 'package:fl_clash/l10n/l10n.dart'; + +final appLocalizations = AppLocalizations.current; \ No newline at end of file diff --git a/lib/common/color.dart b/lib/common/color.dart new file mode 100644 index 0000000..41e9e4a --- /dev/null +++ b/lib/common/color.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +extension ColorExtension on Color { + toLight() { + return withOpacity(0.6); + } + + toLighter() { + return withOpacity(0.4); + } + + toSoft() { + return withOpacity(0.12); + } + + toLittle() { + return withOpacity(0.03); + } +} \ No newline at end of file diff --git a/lib/common/common.dart b/lib/common/common.dart new file mode 100644 index 0000000..a295542 --- /dev/null +++ b/lib/common/common.dart @@ -0,0 +1,25 @@ +export 'path.dart'; +export 'request.dart'; +export 'preferences.dart'; +export 'constant.dart'; +export 'proxy.dart'; +export 'other.dart'; +export 'num.dart'; +export 'navigation.dart'; +export 'window.dart'; +export 'system.dart'; +export 'file.dart'; +export 'android.dart'; +export 'launch.dart'; +export 'protocol.dart'; +export 'datetime.dart'; +export 'context.dart'; +export 'link.dart'; +export 'text.dart'; +export 'color.dart'; +export 'list.dart'; +export 'string.dart'; +export 'app_localizations.dart'; +export 'function.dart'; +export 'package.dart'; +export 'measure.dart'; \ No newline at end of file diff --git a/lib/common/constant.dart b/lib/common/constant.dart new file mode 100644 index 0000000..7c47786 --- /dev/null +++ b/lib/common/constant.dart @@ -0,0 +1,28 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class AppConstant { + final packageName = "com.follow.clash"; + final name = "FlClash"; + final httpTimeoutDuration = const Duration(milliseconds: 5000); + final moreDuration = const Duration(milliseconds: 100); + final defaultUpdateDuration = const Duration(days: 1); + final mmdbFileName = "geoip.metadb"; + final profilesDirectoryName = "profiles"; + final configFileName = "config.yaml"; + final localhost = "127.0.0.1"; + final clashKey = "clash"; + final configKey = "config"; + final listItemPadding = const EdgeInsets.symmetric(horizontal: 16); + final dialogCommonWidth = 300; + final repository = "chen08209/FlClash"; + final filter = ImageFilter.blur( + sigmaX: 5, + sigmaY: 5, + tileMode: TileMode.mirror, + ); + final defaultPrimaryColor = Colors.brown; +} + +final appConstant = AppConstant(); diff --git a/lib/common/context.dart b/lib/common/context.dart new file mode 100644 index 0000000..632c1c0 --- /dev/null +++ b/lib/common/context.dart @@ -0,0 +1,27 @@ +import 'package:fl_clash/application.dart'; +import 'package:fl_clash/controller.dart'; +import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:flutter/material.dart'; + +extension BuildContextExtension on BuildContext { + AppController get appController { + final appController = + findAncestorStateOfType()?.appController; + assert(appController != null, "only use application environment"); + return appController!; + } + + CommonScaffoldState? get commonScaffoldState { + return findAncestorStateOfType(); + } + + double get width { + return MediaQuery.of(this).size.width; + } + + bool get isMobile => width < 600; + + ColorScheme get colorScheme => Theme.of(this).colorScheme; + + TextTheme get textTheme => Theme.of(this).textTheme; +} diff --git a/lib/common/datetime.dart b/lib/common/datetime.dart new file mode 100644 index 0000000..1c730a9 --- /dev/null +++ b/lib/common/datetime.dart @@ -0,0 +1,12 @@ +extension DateTimeExtension on DateTime { + bool isBeforeNow() { + return isBefore(DateTime.now()); + } + + bool isBeforeSecure(DateTime? dateTime) { + if (dateTime == null) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/lib/common/file.dart b/lib/common/file.dart new file mode 100644 index 0000000..006370b --- /dev/null +++ b/lib/common/file.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/models/models.dart'; + +class FileUtil { + static Future> pickerConfig() async { + FilePickerResult? filePickerResult; + if (Platform.isAndroid) { + filePickerResult = await FilePicker.platform.pickFiles( + withData: true, + type: FileType.custom, + allowedExtensions: ['txt', 'conf'], + ); + } else { + filePickerResult = await FilePicker.platform.pickFiles( + withData: true, + type: FileType.custom, + allowedExtensions: ['yaml', 'txt', 'conf'], + ); + } + final file = filePickerResult?.files.first; + if (file == null) { + return Result.error(message: appLocalizations.pleaseUploadFile); + } + return Result.success(data: file); + } +} diff --git a/lib/common/function.dart b/lib/common/function.dart new file mode 100644 index 0000000..7c2f208 --- /dev/null +++ b/lib/common/function.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +class Debouncer { + final Duration delay; + Timer? _timer; + + Debouncer({required this.delay}); + + void call(Function action, List positionalArguments, [Map? namedArguments]) { + _timer?.cancel(); + _timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments)); + } +} + +Function debounce(F func,{int milliseconds = 600}) { + Timer? timer; + + return ([List? args, Map? namedArgs]) { + if (timer != null) { + timer!.cancel(); + } + timer = Timer(Duration(milliseconds: milliseconds), () { + Function.apply(func, args ?? [], namedArgs); + }); + }; +} \ No newline at end of file diff --git a/lib/common/launch.dart b/lib/common/launch.dart new file mode 100644 index 0000000..a00ea0e --- /dev/null +++ b/lib/common/launch.dart @@ -0,0 +1,46 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:launch_at_startup/launch_at_startup.dart'; + +import 'constant.dart'; +import 'system.dart'; + +class AutoLaunch { + static AutoLaunch? _instance; + + AutoLaunch._internal() { + launchAtStartup.setup( + appName: appConstant.name, + appPath: Platform.resolvedExecutable, + ); + } + + factory AutoLaunch() { + _instance ??= AutoLaunch._internal(); + return _instance!; + } + + Future get isEnable async { + return await launchAtStartup.isEnabled(); + } + + Future enable() async { + return await launchAtStartup.enable(); + } + + Future disable() async { + return await launchAtStartup.disable(); + } + + updateStatus(bool value) async { + final isEnable = await this.isEnable; + if (isEnable == value) return; + if (value == true) { + enable(); + } else { + disable(); + } + } +} + +final autoLaunch = system.isDesktop ? AutoLaunch() : null; diff --git a/lib/common/link.dart b/lib/common/link.dart new file mode 100644 index 0000000..8abe7c1 --- /dev/null +++ b/lib/common/link.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:app_links/app_links.dart'; +import 'package:flutter/cupertino.dart'; + +typedef InstallConfigCallBack = void Function(String url); + +class LinkManager { + static LinkManager? _instance; + late AppLinks _appLinks; + StreamSubscription? subscription; + + LinkManager._internal() { + _appLinks = AppLinks(); + } + + initAppLinksListen(installConfigCallBack) async { + debugPrint("initAppLinksListen"); + destroy(); + subscription = _appLinks.allUriLinkStream.listen( + (uri) { + debugPrint('onAppLink: $uri'); + if (uri.host == 'install-config') { + final parameters = uri.queryParameters; + final url = parameters['url']; + if (url != null) { + installConfigCallBack(url); + } + } + }, + ); + } + + + destroy(){ + if (subscription != null) { + subscription?.cancel(); + subscription = null; + } + } + + factory LinkManager() { + _instance ??= LinkManager._internal(); + return _instance!; + } +} + +final linkManager = LinkManager(); diff --git a/lib/common/list.dart b/lib/common/list.dart new file mode 100644 index 0000000..ca36db0 --- /dev/null +++ b/lib/common/list.dart @@ -0,0 +1,5 @@ +extension ListExtension on List { + List intersection(List list) { + return where((item) => list.contains(item)).toList(); + } +} \ No newline at end of file diff --git a/lib/common/measure.dart b/lib/common/measure.dart new file mode 100644 index 0000000..b6de767 --- /dev/null +++ b/lib/common/measure.dart @@ -0,0 +1,68 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class Measure { + Measure.of(this.context); + + final _textScaleFactor = + WidgetsBinding.instance.platformDispatcher.textScaleFactor; + + Size computeTextSize(Text text) { + final textPainter = TextPainter( + text: TextSpan(text: text.data, style: text.style), + maxLines: text.maxLines, + textScaler: TextScaler.linear(_textScaleFactor), + textDirection: text.textDirection ?? TextDirection.ltr, + )..layout(); + return textPainter.size; + } + + late BuildContext context; + + double? _bodyMediumHeight; + double? _bodySmallHeight; + double? _labelSmallHeight; + double? _titleLargeHeight; + + + double get bodyMediumHeight{ + _bodyMediumHeight ??= computeTextSize( + Text( + "", + style: context.textTheme.bodyMedium, + ), + ).height; + return _bodyMediumHeight!; + } + + double get bodySmallHeight{ + _bodySmallHeight ??= computeTextSize( + Text( + "", + style: context.textTheme.bodySmall, + ), + ).height; + return _bodySmallHeight!; + } + + double get labelSmallHeight{ + _labelSmallHeight ??= computeTextSize( + Text( + "", + style: context.textTheme.labelSmall, + ), + ).height; + return _labelSmallHeight!; + } + + double get titleLargeHeight{ + _titleLargeHeight ??= computeTextSize( + Text( + "", + style: context.textTheme.titleLarge, + ), + ).height; + return _titleLargeHeight!; + } +} diff --git a/lib/common/navigation.dart b/lib/common/navigation.dart new file mode 100644 index 0000000..99d8d42 --- /dev/null +++ b/lib/common/navigation.dart @@ -0,0 +1,58 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/fragments/fragments.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; + +class Navigation { + static Navigation? _instance; + + getItems({ + bool openLogs = false, + bool hasProxies = false, + }) { + return [ + const NavigationItem( + icon: Icon(Icons.space_dashboard), + label: "dashboard", + fragment: DashboardFragment(), + ), + NavigationItem( + icon: const Icon(Icons.rocket), + label: "proxies", + fragment: const ProxiesFragment(), + modes: hasProxies + ? [NavigationItemMode.mobile, NavigationItemMode.desktop] + : [], + ), + const NavigationItem( + icon: Icon(Icons.folder), + label: "profiles", + fragment: ProfilesFragment(), + ), + NavigationItem( + icon: const Icon(Icons.adb), + label: "logs", + fragment: const LogsFragment(), + description: "logsDesc", + modes: openLogs + ? [NavigationItemMode.desktop, NavigationItemMode.more] + : [], + ), + const NavigationItem( + icon: Icon(Icons.construction), + label: "tools", + fragment: ToolsFragment(), + modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], + ), + ]; + } + + Navigation._internal(); + + factory Navigation() { + _instance ??= Navigation._internal(); + return _instance!; + } +} + +final navigation = Navigation(); diff --git a/lib/common/num.dart b/lib/common/num.dart new file mode 100644 index 0000000..9b5a435 --- /dev/null +++ b/lib/common/num.dart @@ -0,0 +1,5 @@ +extension NumExtension on num { + String fixed({digit = 2}) { + return toStringAsFixed(truncateToDouble() == this ? 0 : digit); + } +} diff --git a/lib/common/other.dart b/lib/common/other.dart new file mode 100644 index 0000000..435da1f --- /dev/null +++ b/lib/common/other.dart @@ -0,0 +1,136 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class Other { + static Color? getDelayColor(int? delay) { + if (delay == null) return null; + if (delay < 0) return Colors.red; + if (delay < 600) return Colors.green; + return const Color(0xFFC57F0A); + } + + static String getDateStringLast2(int value) { + var valueRaw = "0$value"; + return valueRaw.substring( + valueRaw.length - 2, + ); + } + + static String getTimeDifference(DateTime dateTime) { + var currentDateTime = DateTime.now(); + var difference = currentDateTime.difference(dateTime); + var inHours = difference.inHours; + var inMinutes = difference.inMinutes; + var inSeconds = difference.inSeconds; + + return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}"; + } + + static String getTimeText(int? timeStamp) { + if (timeStamp == null) { + return '00:00:00'; + } + final diff = timeStamp / 1000; + final inHours = (diff / 3600).floor(); + final inMinutes = (diff / 60 % 60).floor(); + final inSeconds = (diff % 60).floor(); + + return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}"; + } + + static Locale? getLocaleForString(String? localString) { + if (localString == null) return null; + var localSplit = localString.split("_"); + if (localSplit.length == 1) { + return Locale(localSplit[0]); + } + if (localSplit.length == 2) { + return Locale(localSplit[0], localSplit[1]); + } + if (localSplit.length == 3) { + return Locale.fromSubtags( + languageCode: localSplit[0], + scriptCode: localSplit[1], + countryCode: localSplit[2]); + } + return null; + } + + static int sortByChar(String a, String b) { + if (a.isEmpty && b.isEmpty) { + return 0; + } + if (a.isEmpty) { + return -1; + } + if (b.isEmpty) { + return 1; + } + final charA = a[0]; + final charB = b[0]; + + if (charA == charB) { + return sortByChar(a.substring(1), b.substring(1)); + } else { + return charA.compareTo(charB); + } + } + + static String getOverwriteLabel(String label) { + final reg = RegExp(r'\((\d+)\)$'); + final matches = reg.allMatches(label); + if (matches.isNotEmpty) { + final match = matches.last; + final number = int.parse(match[1] ?? '0') + 1; + return label.replaceFirst(reg, '($number)', label.length - 3 - 1); + } else { + return "$label(1)"; + } + } + + // static FutureOr Function(T p) debounce(void Function(T? p) func, + // {Duration? duration}) { + // Timer? timer; + // return ([T? p]) { + // if (timer != null) { + // timer?.cancel(); + // } + // timer = Timer(duration ?? const Duration(milliseconds: 300), () { + // func(p); + // }); + // }; + // } + + + static String getTrayIconPath() { + if (Platform.isWindows) { + return "assets/images/app_icon.ico"; + } else { + return "assets/images/launch_icon.png"; + } + } + + static int compareVersions(String version1, String version2) { + List v1 = version1.split('+')[0].split('.'); + List v2 = version2.split('+')[0].split('.'); + int major1 = int.parse(v1[0]); + int major2 = int.parse(v2[0]); + if (major1 != major2) { + return major1.compareTo(major2); + } + int minor1 = v1.length > 1 ? int.parse(v1[1]) : 0; + int minor2 = v2.length > 1 ? int.parse(v2[1]) : 0; + if (minor1 != minor2) { + return minor1.compareTo(minor2); + } + int patch1 = v1.length > 2 ? int.parse(v1[2]) : 0; + int patch2 = v2.length > 2 ? int.parse(v2[2]) : 0; + if (patch1 != patch2) { + return patch1.compareTo(patch2); + } + int build1 = version1.contains('+') ? int.parse(version1.split('+')[1]) : 0; + int build2 = version2.contains('+') ? int.parse(version2.split('+')[1]) : 0; + return build1.compareTo(build2); + } +} diff --git a/lib/common/package.dart b/lib/common/package.dart new file mode 100644 index 0000000..68eb8dd --- /dev/null +++ b/lib/common/package.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:package_info_plus/package_info_plus.dart'; + +class AppPackage{ + + static AppPackage? _instance; + Completer packageInfoCompleter = Completer(); + + AppPackage._internal() { + PackageInfo.fromPlatform().then( + (value) => packageInfoCompleter.complete(value), + ); + } + + factory AppPackage() { + _instance ??= AppPackage._internal(); + return _instance!; + } +} + +final appPackage = AppPackage(); \ No newline at end of file diff --git a/lib/common/path.dart b/lib/common/path.dart new file mode 100644 index 0000000..18449ad --- /dev/null +++ b/lib/common/path.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'constant.dart'; + +class AppPath { + static AppPath? _instance; + Completer applicationSupportDirectoryCompleter = Completer(); + + AppPath._internal() { + getApplicationSupportDirectory().then( + (value) => applicationSupportDirectoryCompleter.complete(value), + ); + } + + factory AppPath() { + _instance ??= AppPath._internal(); + return _instance!; + } + + Future getHomeDirPath() async { + final directory = await applicationSupportDirectoryCompleter.future; + return directory.path; + } + + Future getConfigPath() async { + final directory = await applicationSupportDirectoryCompleter.future; + return join(directory.path, appConstant.configFileName); + } + + Future getProfilesPath() async { + final directory = await applicationSupportDirectoryCompleter.future; + return join(directory.path, appConstant.profilesDirectoryName); + } + + Future getProfilePath(String? id) async { + if (id == null) return null; + final directory = await getProfilesPath(); + return join(directory, "$id.yaml"); + } + + Future getMMDBPath() async { + var directory = await applicationSupportDirectoryCompleter.future; + return join(directory.path, appConstant.mmdbFileName); + } +} + +final appPath = AppPath(); diff --git a/lib/common/preferences.dart b/lib/common/preferences.dart new file mode 100644 index 0000000..664a965 --- /dev/null +++ b/lib/common/preferences.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../models/models.dart'; +import 'constant.dart'; + +class Preferences { + static Preferences? _instance; + Completer sharedPreferencesCompleter = Completer(); + + Preferences._internal() { + SharedPreferences.getInstance() + .then((value) => sharedPreferencesCompleter.complete(value)); + } + + factory Preferences() { + _instance ??= Preferences._internal(); + return _instance!; + } + + Future getClashConfig() async { + final preferences = await sharedPreferencesCompleter.future; + final clashConfigString = preferences.getString(appConstant.clashKey); + if (clashConfigString == null) return null; + final clashConfigMap = json.decode(clashConfigString); + try { + return ClashConfig.fromJson(clashConfigMap); + } catch (e) { + debugPrint(e.toString()); + return null; + } + } + + Future saveClashConfig(ClashConfig clashConfig) async { + final preferences = await sharedPreferencesCompleter.future; + return preferences.setString( + appConstant.clashKey, + json.encode(clashConfig), + ); + } + + Future getConfig() async { + final preferences = await sharedPreferencesCompleter.future; + final configString = preferences.getString(appConstant.configKey); + if (configString == null) return null; + final configMap = json.decode(configString); + try { + return Config.fromJson(configMap); + } catch (e) { + debugPrint(e.toString()); + return null; + } + } + + Future saveConfig(Config config) async { + final preferences = await sharedPreferencesCompleter.future; + return preferences.setString( + appConstant.configKey, + json.encode(config), + ); + } + + clearPreferences() async { + final sharedPreferencesIns = await sharedPreferencesCompleter.future; + sharedPreferencesIns.clear(); + } +} + +final preferences = Preferences(); \ No newline at end of file diff --git a/lib/common/protocol.dart b/lib/common/protocol.dart new file mode 100644 index 0000000..a4fedf2 --- /dev/null +++ b/lib/common/protocol.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:win32_registry/win32_registry.dart'; + +class Protocol { + static Protocol? _instance; + + Protocol._internal(); + + factory Protocol() { + _instance ??= Protocol._internal(); + return _instance!; + } + + void register(String scheme) { + String protocolRegKey = 'Software\\Classes\\$scheme'; + RegistryValue protocolRegValue = const RegistryValue( + 'URL Protocol', + RegistryValueType.string, + '', + ); + String protocolCmdRegKey = 'shell\\open\\command'; + RegistryValue protocolCmdRegValue = RegistryValue( + '', + RegistryValueType.string, + '"${Platform.resolvedExecutable}" "%1"', + ); + final regKey = Registry.currentUser.createKey(protocolRegKey); + regKey.createValue(protocolRegValue); + regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); + } +} + +final protocol = Protocol(); \ No newline at end of file diff --git a/lib/common/proxy.dart b/lib/common/proxy.dart new file mode 100644 index 0000000..9019665 --- /dev/null +++ b/lib/common/proxy.dart @@ -0,0 +1,51 @@ +import 'package:fl_clash/common/datetime.dart'; +import 'package:fl_clash/plugins/proxy.dart'; +import 'package:proxy/proxy.dart' as proxy_plugin; +import 'package:proxy/proxy_platform_interface.dart'; + +class ProxyManager { + static ProxyManager? _instance; + late ProxyPlatform _proxy; + + ProxyManager._internal() { + _proxy = proxy ?? proxy_plugin.Proxy(); + } + + bool get isStart => startTime != null && startTime!.isBeforeNow(); + + DateTime? get startTime => _proxy.startTime; + + Future startProxy({required int port, String? args}) async { + return await _proxy.startProxy(port, args); + } + + Future stopProxy() async { + return await _proxy.stopProxy(); + } + + Future updateStartTime() async { + if (_proxy is! Proxy) return null; + return await (_proxy as Proxy).updateStartTime(); + } + + Future setProtect(int fd) async { + if (_proxy is! Proxy) return null; + return await (_proxy as Proxy).setProtect(fd); + } + + Future startForeground({ + required String title, + required String content, + }) async { + if (_proxy is! Proxy) return null; + return await (_proxy as Proxy) + .startForeground(title: title, content: content); + } + + factory ProxyManager() { + _instance ??= ProxyManager._internal(); + return _instance!; + } +} + +final proxyManager = ProxyManager(); diff --git a/lib/common/request.dart b/lib/common/request.dart new file mode 100644 index 0000000..2be27a8 --- /dev/null +++ b/lib/common/request.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:fl_clash/common/common.dart'; +import 'package:http/http.dart'; +import '../models/models.dart'; + +class Request { + static Future> getFileResponseForUrl(String url) async { + final headers = {'User-Agent': appConstant.name}; + try { + final response = await get(Uri.parse(url), headers: headers).timeout( + appConstant.httpTimeoutDuration, + ); + return Result.success(data: response); + } catch (err) { + return Result.error(message: err.toString()); + } + } + + static Future> checkForUpdate() async { + final response = await get( + Uri.parse( + "https://api.github.com/repos/${appConstant.repository}/releases/latest", + ), + ); + if (response.statusCode != 200) return Result.error(); + final body = json.decode(response.body); + final remoteVersion = body['tag_name']; + final packageInfo = await appPackage.packageInfoCompleter.future; + final version = packageInfo.version; + final hasUpdate = + Other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; + if (!hasUpdate) return Result.error(); + return Result.success(data: body['body']); + } +} diff --git a/lib/common/string.dart b/lib/common/string.dart new file mode 100644 index 0000000..b5160ca --- /dev/null +++ b/lib/common/string.dart @@ -0,0 +1,10 @@ +extension StringExtension on String { + bool get isUrl { + try { + Uri.parse(this); + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/common/system.dart b/lib/common/system.dart new file mode 100644 index 0000000..c7e4174 --- /dev/null +++ b/lib/common/system.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:fl_clash/plugins/app.dart'; +import 'package:flutter/services.dart'; + +import 'window.dart'; + +class System { + static System? _instance; + + System._internal(); + + factory System() { + _instance ??= System._internal(); + return _instance!; + } + + bool get isDesktop => + Platform.isWindows || Platform.isMacOS || Platform.isLinux; + + back() async { + await app?.moveTaskToBack(); + await window?.hide(); + } + + exit() async { + if (Platform.isAndroid) { + await SystemNavigator.pop(); + } + await window?.close(); + } +} + +final system = System(); diff --git a/lib/common/text.dart b/lib/common/text.dart new file mode 100644 index 0000000..78b011b --- /dev/null +++ b/lib/common/text.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'color.dart'; + +extension TextStyleExtension on TextStyle { + toLight() { + return copyWith(color: color?.toLight()); + } + + toSoftBold() { + return copyWith(fontWeight: FontWeight.w500); + } + + toBold() { + return copyWith(fontWeight: FontWeight.bold); + } +} \ No newline at end of file diff --git a/lib/common/window.dart b/lib/common/window.dart new file mode 100644 index 0000000..701e0fb --- /dev/null +++ b/lib/common/window.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:windows_single_instance/windows_single_instance.dart'; + +import 'protocol.dart'; +import 'system.dart'; + +class Window { + init() async { + if (Platform.isWindows) { + await WindowsSingleInstance.ensureSingleInstance([], "FlClash"); + protocol.register("clash"); + protocol.register("clashmeta"); + protocol.register("flclash"); + } + await windowManager.ensureInitialized(); + WindowOptions windowOptions = const WindowOptions( + size: Size(1000, 600), + minimumSize: Size(400, 600), + center: true, + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setPreventClose(true); + }); + } + + show() async { + await windowManager.show(); + await windowManager.focus(); + } + + close() async { + exit(0); + } + + hide() async { + await windowManager.hide(); + } +} + +final window = system.isDesktop ? Window() : null; diff --git a/lib/controller.dart b/lib/controller.dart new file mode 100644 index 0000000..418ead3 --- /dev/null +++ b/lib/controller.dart @@ -0,0 +1,358 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'clash/core.dart'; +import 'enum/enum.dart'; +import 'models/models.dart'; +import 'common/common.dart'; + +class AppController { + final BuildContext context; + late AppState appState; + late Config config; + late ClashConfig clashConfig; + late Measure measure; + late Function updateClashConfigDebounce; + + AppController(this.context) { + appState = context.read(); + config = context.read(); + clashConfig = context.read(); + updateClashConfigDebounce = debounce(() async { + await updateClashConfig(); + }); + measure = Measure.of(context); + } + + Future updateSystemProxy(bool isStart) async { + if (isStart) { + await globalState.startSystemProxy( + config: config, + clashConfig: clashConfig, + ); + + updateRunTime(); + updateTraffic(); + globalState.updateFunctionLists = [ + updateRunTime, + updateTraffic, + ]; + } else { + await globalState.stopSystemProxy(); + appState.traffics = []; + appState.runTime = null; + } + } + + updateCoreVersionInfo() { + globalState.updateCoreVersionInfo(appState); + } + + updateRunTime() { + if (proxyManager.startTime != null) { + final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch; + final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; + appState.runTime = nowTimeStamp - startTimeStamp; + } else { + appState.runTime = null; + } + } + + updateTraffic() { + globalState.updateTraffic( + config: config, + appState: appState, + ); + } + + changeProxy() { + final currentGroupName = + appState.getCurrentGroupName(config.currentGroupName, clashConfig.mode); + final currentProxyName = + appState.getCurrentProxyName(config.currentProxyName, clashConfig.mode); + if (config.profiles.isEmpty || currentProxyName == null) { + updateSystemProxy(false); + return; + } + if (currentGroupName == null) return; + final groupIndex = appState.groups.indexWhere( + (element) => element.name == currentGroupName, + ); + if (groupIndex == -1) return; + final proxyIndex = appState.groups[groupIndex].all.indexWhere( + (element) => element.name == currentProxyName, + ); + if (proxyIndex == -1) return; + clashCore.changeProxy( + ChangeProxyParams( + groupName: currentGroupName, + proxyName: currentProxyName, + ), + ); + } + + addProfile(Profile profile) { + config.setProfile(profile); + if (config.currentProfileId != null) return; + changeProfile(profile.id); + } + + deleteProfile(String id) async { + config.deleteProfileById(id); + final profilePath = await appPath.getProfilePath(id); + if (profilePath == null) return; + final file = File(profilePath); + Isolate.run(() async { + final isExists = await file.exists(); + if (isExists) { + file.delete(); + } + }); + if (config.currentProfileId == id) { + if (config.profiles.isNotEmpty) { + final updateId = config.profiles.first.id; + changeProfile(updateId); + } else { + changeProfile(null); + } + } + } + + updateProfile(String id) async { + final profile = config.getCurrentProfileForId(id); + if (profile != null) { + final res = await profile.update(); + if (res.type == ResultType.success) { + config.setProfile(profile); + } + } + } + + Future updateClashConfig({bool isPatch = true}) async { + return await globalState.updateClashConfig( + clashConfig: clashConfig, + config: config, + isPatch: isPatch, + ); + } + + changeProfile(String? value) async { + if (value == config.currentProfileId) return; + config.currentProfileId = value; + await updateClashConfig(isPatch: false); + updateGroups(); + changeProxy(); + appState.delayMap = {}; + saveConfigPreferences(); + } + + autoUpdateProfiles() async { + for (final profile in config.profiles) { + if (!profile.autoUpdate) return; + final isNotNeedUpdate = profile.lastUpdateDate + ?.add( + profile.autoUpdateDuration, + ) + .isBeforeNow(); + if (isNotNeedUpdate == false) continue; + final result = await profile.update(); + if (result.type == ResultType.error) continue; + updateGroups(); + changeProxy(); + } + } + + updateGroups() { + globalState.updateGroups(appState); + } + + updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) { + appState.systemColorSchemes = systemColorSchemes; + } + + clearCurrentDelay() { + final currentProxyName = + appState.getCurrentProxyName(config.currentProxyName, clashConfig.mode); + if (currentProxyName == null) return; + appState.setDelay(Delay(name: currentProxyName, value: null)); + } + + savePreferences() async { + await saveConfigPreferences(); + await saveClashConfigPreferences(); + } + + saveConfigPreferences() async { + debugPrint("saveConfigPreferences"); + await preferences.saveConfig(config); + } + + saveClashConfigPreferences() async { + debugPrint("saveClashConfigPreferences"); + await preferences.saveClashConfig(clashConfig); + } + + handleBackOrExit() async { + if (config.isMinimizeOnExit) { + if (system.isDesktop) { + await savePreferences(); + } + await system.back(); + } else { + await handleExit(); + } + } + + handleExit() async { + await updateSystemProxy(false); + await savePreferences(); + clashCore.shutdown(); + system.exit(); + } + + updateLogStatus() { + if (config.openLogs) { + clashCore.startLog(); + } else { + clashCore.stopLog(); + appState.logs = []; + } + } + + afterInit() async { + if (appState.isInit) { + changeProxy(); + if (config.autoRun) { + await updateSystemProxy(true); + } else { + await proxyManager.updateStartTime(); + await updateSystemProxy(proxyManager.isStart); + } + autoUpdateProfiles(); + updateLogStatus(); + if (!config.silentLaunch) { + window?.show(); + } + } + } + + setDelay(Delay delay) { + appState.setDelay(delay); + } + + toPage(int index, {bool hasAnimate = false}) { + final nextLabel = globalState.currentNavigationItems[index].label; + appState.currentLabel = nextLabel; + if ((config.isAnimateToPage || hasAnimate)) { + globalState.pageController?.animateToPage( + index, + duration: kTabScrollDuration, + curve: Curves.easeOut, + ); + } else { + globalState.pageController?.jumpToPage(index); + } + } + + updatePackages() async { + await globalState.updatePackages(appState); + } + + toProfiles() { + final index = globalState.currentNavigationItems.indexWhere( + (element) => element.label == "profiles", + ); + if (index != -1) { + toPage(index); + } + } + + addProfileFormURL(String url) async { + globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); + toProfiles(); + final commonScaffoldState = globalState.homeScaffoldKey.currentState; + if (commonScaffoldState?.mounted != true) return; + commonScaffoldState?.loadingRun( + () async { + await Future.delayed(const Duration(milliseconds: 300)); + final profile = Profile( + url: url, + ); + final res = await profile.update(); + if (res.type == ResultType.success) { + addProfile(profile); + } else { + debugPrint(res.message); + globalState.showMessage( + title: "${appLocalizations.add}${appLocalizations.profile}", + message: TextSpan(text: res.message!), + ); + } + }, + ); + } + + initLink() { + linkManager.initAppLinksListen( + (url) { + globalState.showMessage( + title: "${appLocalizations.add}${appLocalizations.profile}", + message: TextSpan( + children: [ + TextSpan(text: appLocalizations.doYouWantToPass), + TextSpan( + text: " $url ", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: Theme.of(context).colorScheme.primary, + ), + ), + TextSpan( + text: + "${appLocalizations.create}${appLocalizations.profile}"), + ], + ), + onTab: () { + addProfileFormURL(url); + }, + ); + }, + ); + } + + addProfileFormFile() async { + final result = await FileUtil.pickerConfig(); + if (result.type == ResultType.error) return; + if (!context.mounted) return; + globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); + toProfiles(); + final commonScaffoldState = globalState.homeScaffoldKey.currentState; + if (commonScaffoldState?.mounted != true) return; + commonScaffoldState?.loadingRun( + () async { + await Future.delayed(const Duration(milliseconds: 300)); + final bytes = result.data?.bytes; + if (bytes == null) { + return; + } + final profile = Profile(label: result.data?.name); + final sRes = await profile.saveFile(bytes); + if (sRes.type == ResultType.error) { + debugPrint(sRes.message); + globalState.showMessage( + title: "${appLocalizations.add}${appLocalizations.profile}", + message: TextSpan(text: sRes.message!), + ); + return; + } + addProfile(profile); + }, + ); + } +} diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart new file mode 100644 index 0000000..d30c856 --- /dev/null +++ b/lib/enum/enum.dart @@ -0,0 +1,56 @@ +// ignore_for_file: constant_identifier_names + +enum GroupType { Selector, URLTest, Fallback } + +extension GroupTypeExtension on GroupType { + static List get valueList => GroupType.values + .map( + (e) => e.toString().split(".").last, + ) + .toList(); + + static GroupType? getGroupType(String? value) { + if (value == null) return null; + final index = GroupTypeExtension.valueList.indexOf(value); + if (index == -1) return null; + return GroupType.values[index]; + } + + String get value => GroupTypeExtension.valueList[index]; +} + +enum UsedProxy { GLOBAL, DIRECT, REJECT } + +extension UsedProxyExtension on UsedProxy { + static List get valueList => UsedProxy.values + .map( + (e) => e.toString().split(".").last, + ) + .toList(); + + String get value => UsedProxyExtension.valueList[index]; +} + +enum Mode { rule, global, direct } + +enum LogLevel { debug, info, warning, error, silent } + +enum TransportProtocol { udp, tcp } + +enum TrafficUnit { B, KB, MB, GB, TB } + +enum NavigationItemMode { mobile, desktop, more } + +enum Network { tcp, udp } + +enum ProxiesSortType { none, delay, name } + +enum TunStack { gvisor, system, mixed } + +enum AccessControlMode { acceptSelected, rejectSelected } + +enum ProfileType { file, url } + +enum ResultType { success, error } + +enum MessageType { log, tun, delay, process } diff --git a/lib/fragments/about.dart b/lib/fragments/about.dart new file mode 100644 index 0000000..7fd63a1 --- /dev/null +++ b/lib/fragments/about.dart @@ -0,0 +1,104 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AboutFragment extends StatelessWidget { + const AboutFragment({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + padding: kMaterialListPadding.copyWith( + top: 16, + bottom: 16, + ), + children: [ + ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Image.asset( + 'assets/images/launch_icon.png', + width: 100, + height: 100, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appConstant.name, + style: Theme.of(context).textTheme.headlineSmall, + ), + FutureBuilder( + future: appPackage.packageInfoCompleter.future, + builder: (_, package) { + final version = package.data?.version; + if (version == null) return Container(); + return Text( + version, + style: Theme.of(context).textTheme.labelLarge, + ); + }, + ) + ], + ) + ], + ), + const SizedBox( + height: 24, + ), + Text( + appLocalizations.desc, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ListTile( + title: Text(appLocalizations.checkUpdate), + onTap: () { + final commonScaffoldState = context.commonScaffoldState; + if (commonScaffoldState?.mounted != true) return; + commonScaffoldState?.loadingRun(() async { + await globalState.checkUpdate( + () { + launchUrl( + Uri.parse( + "https://github.com/${appConstant.repository}/releases/latest"), + ); + }, + ); + }); + }, + ), + ListTile( + title: Text(appLocalizations.project), + onTap: () { + launchUrl( + Uri.parse("https://github.com/${appConstant.repository}"), + ); + }, + trailing: const Icon(Icons.launch), + ), + ListTile( + title: Text(appLocalizations.core), + onTap: () { + launchUrl( + Uri.parse("https://github.com/chen08209/Clash.Meta/tree/FlClash"), + ); + }, + trailing: const Icon(Icons.launch), + ), + ], + ); + } +} diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart new file mode 100644 index 0000000..bf2534f --- /dev/null +++ b/lib/fragments/access.dart @@ -0,0 +1,333 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/plugins/app.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AccessFragment extends StatelessWidget { + const AccessFragment({super.key}); + + Widget _buildPackageItem({ + required Package package, + required bool value, + required bool isActive, + required void Function(bool?) onChanged, + }) { + return AbsorbPointer( + absorbing: !isActive, + child: ListItem.checkbox( + leading: SizedBox( + width: 48, + height: 48, + child: FutureBuilder( + future: app?.getPackageIcon(package.packageName), + builder: (_, snapshot) { + if (!snapshot.hasData && snapshot.data == null) { + return Container(); + } else { + return Image( + image: snapshot.data!, + gaplessPlayback: true, + width: 48, + height: 48, + ); + } + }, + ), + ), + title: Text( + package.label, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + subtitle: Text( + package.packageName, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + delegate: CheckboxDelegate( + value: value, + onChanged: onChanged, + ), + ), + ); + } + + Widget _buildAppProxyModePopup() { + final items = [ + CommonPopupMenuItem( + action: AccessControlMode.rejectSelected, + label: appLocalizations.blacklistMode, + ), + CommonPopupMenuItem( + action: AccessControlMode.acceptSelected, + label: appLocalizations.whitelistMode, + ), + ]; + return Selector( + selector: (_, config) => config.accessControl.mode, + builder: (context, mode, __) { + return CommonPopupMenu.radio( + icon: Icon( + Icons.mode_standby, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + items: items, + onSelected: (value) { + final config = context.read(); + config.accessControl = config.accessControl.copyWith( + mode: value, + ); + }, + selectedValue: mode, + ); + }, + ); + } + + Widget _buildFilterSystemAppButton() { + return Selector( + selector: (_, config) => config.accessControl.isFilterSystemApp, + builder: (context, isFilterSystemApp, __) { + final tooltip = isFilterSystemApp + ? appLocalizations.cancelFilterSystemApp + : appLocalizations.filterSystemApp; + return IconButton( + tooltip: tooltip, + onPressed: () { + final config = context.read(); + config.accessControl = config.accessControl.copyWith( + isFilterSystemApp: !isFilterSystemApp, + ); + }, + icon: isFilterSystemApp + ? const Icon(Icons.filter_list_off) + : const Icon(Icons.filter_list), + ); + }, + ); + } + + Widget _buildSelectedAllButton({ + required bool isSelectedAll, + required List allValueList, + }) { + return Builder( + builder: (context) { + final tooltip = isSelectedAll + ? appLocalizations.cancelSelectAll + : appLocalizations.selectAll; + return IconButton( + tooltip: tooltip, + onPressed: () { + final config = context.read(); + if (isSelectedAll) { + config.accessControl.currentList = []; + config.accessControl = config.accessControl.copyWith(); + } else { + config.accessControl.currentList = allValueList; + config.accessControl = config.accessControl.copyWith(); + } + }, + icon: isSelectedAll + ? const Icon(Icons.deselect) + : const Icon(Icons.select_all), + ); + }, + ); + } + + Widget _buildPackageList(bool isAccessControl) { + return Selector2( + selector: (_, appState, config) => PackageListSelectorState( + accessControl: config.accessControl, + packages: appState.packages, + ), + builder: (context, state, __) { + final accessControl = state.accessControl; + final isFilterSystemApp = accessControl.isFilterSystemApp; + final packages = isFilterSystemApp + ? state.packages + .where((element) => element.isSystem == false) + .toList() + : state.packages; + final packageNameList = packages.map((e) => e.packageName).toList(); + final accessControlMode = accessControl.mode; + final valueList = + accessControl.currentList.intersection(packageNameList); + final describe = accessControlMode == AccessControlMode.acceptSelected + ? appLocalizations.accessControlAllowDesc + : appLocalizations.accessControlNotAllowDesc; + + final listView = ListView.builder( + itemCount: packages.length, + itemBuilder: (_, index) { + final package = packages[index]; + return _buildPackageItem( + package: package, + value: valueList.contains(package.packageName), + isActive: isAccessControl, + onChanged: (value) { + if (value == true) { + valueList.add(package.packageName); + } else { + valueList.remove(package.packageName); + } + final config = context.read(); + config.accessControl.currentList = valueList; + config.accessControl = config.accessControl.copyWith(); + }, + ); + }, + ); + + return DisabledMask( + status: !isAccessControl, + child: Column( + children: [ + AbsorbPointer( + absorbing: !isAccessControl, + child: Padding( + padding: const EdgeInsets.only( + top: 4, + bottom: 4, + left: 16, + right: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Flexible( + child: Text( + appLocalizations.selected, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + const Flexible( + child: SizedBox( + width: 8, + ), + ), + Flexible( + child: Text( + "${valueList.length}", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + ], + ), + ), + Flexible( + child: Text(describe), + ) + ], + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _buildSelectedAllButton( + isSelectedAll: + valueList.length == packageNameList.length, + allValueList: packageNameList, + ), + ), + Flexible(child: _buildFilterSystemAppButton()), + Flexible(child: _buildAppProxyModePopup()), + ], + ), + ], + ), + ), + ), + Flexible( + flex: 1, + child: FadeBox( + child: packages.isEmpty + ? const Center( + child: CircularProgressIndicator(), + ) + : listView, + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + if (context.appController.appState.packages.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.appController.updatePackages(); + }); + } + return Selector( + selector: (_, config) => config.isAccessControl, + builder: (_, isAccessControl, __) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + flex: 0, + child: ListItem.switchItem( + title: Text(appLocalizations.appAccessControl), + delegate: SwitchDelegate( + value: isAccessControl, + onChanged: (isAccessControl) { + final config = context.read(); + config.isAccessControl = isAccessControl; + }, + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Divider( + height: 12, + ), + ), + Flexible( + child: _buildPackageList(isAccessControl), + ), + ], + ); + }, + ); + } +} diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart new file mode 100644 index 0000000..2cf2ac0 --- /dev/null +++ b/lib/fragments/application_setting.dart @@ -0,0 +1,141 @@ +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class ApplicationSettingFragment extends StatelessWidget { + const ApplicationSettingFragment({super.key}); + + String getLocaleString(Locale? locale) { + if (locale == null) return appLocalizations.defaultText; + return Intl.message(locale.toString()); + } + + @override + Widget build(BuildContext context) { + List items = [ + Selector( + selector: (_, config) => config.isMinimizeOnExit, + builder: (_, isMinimizeOnExit, child) { + return ListItem.switchItem( + leading: const Icon(Icons.back_hand), + title: Text(appLocalizations.minimizeOnExit), + subtitle: Text(appLocalizations.minimizeOnExitDesc), + delegate: SwitchDelegate( + value: isMinimizeOnExit, + onChanged: (bool value) { + final config = context.read(); + config.isMinimizeOnExit = value; + }, + ), + ); + }, + ), + if (system.isDesktop) + Selector( + selector: (_, config) => config.autoLaunch, + builder: (_, autoLaunch, child) { + return ListItem.switchItem( + leading: const Icon(Icons.rocket_launch), + title: Text(appLocalizations.autoLaunch), + subtitle: Text(appLocalizations.autoLaunchDesc), + delegate: SwitchDelegate( + value: autoLaunch, + onChanged: (bool value) { + final config = context.read(); + config.autoLaunch = value; + }, + ), + ); + }, + ), + if (system.isDesktop) + Selector( + selector: (_, config) => config.silentLaunch, + builder: (_, silentLaunch, child) { + return ListItem.switchItem( + leading: const Icon(Icons.expand_circle_down), + title: Text(appLocalizations.silentLaunch), + subtitle: Text(appLocalizations.silentLaunchDesc), + delegate: SwitchDelegate( + value: silentLaunch, + onChanged: (bool value) { + final config = context.read(); + config.silentLaunch = value; + }, + ), + ); + }, + ), + Selector( + selector: (_, config) => config.autoRun, + builder: (_, autoRun, child) { + return ListItem.switchItem( + leading: const Icon(Icons.start), + title: Text(appLocalizations.autoRun), + subtitle: Text(appLocalizations.autoRunDesc), + delegate: SwitchDelegate( + value: autoRun, + onChanged: (bool value) { + final config = context.read(); + config.autoRun = value; + }, + ), + ); + }, + ), + Selector( + selector: (_, config) => config.openLogs, + builder: (_, openLogs, child) { + return ListItem.switchItem( + leading: const Icon(Icons.bug_report), + title: Text(appLocalizations.logcat), + subtitle: Text(appLocalizations.logcatDesc), + delegate: SwitchDelegate( + value: openLogs, + onChanged: (bool value) { + final config = context.read(); + config.openLogs = value; + context.appController.updateLogStatus(); + }, + ), + ); + }, + ), + if (Platform.isAndroid) + Selector( + selector: (_, config) => config.isAnimateToPage, + builder: (_, isAnimateToPage, child) { + return ListItem.switchItem( + leading: const Icon(Icons.animation), + title: Text(appLocalizations.tabAnimation), + subtitle: Text(appLocalizations.tabAnimationDesc), + delegate: SwitchDelegate( + value: isAnimateToPage, + onChanged: (value) { + final config = context.read(); + config.isAnimateToPage = value; + }, + ), + ); + }, + ), + ]; + return ListView.separated( + itemBuilder: (_, index) { + final item = items[index]; + return item; + }, + separatorBuilder: (_, __) { + return const Divider( + height: 0, + ); + }, + itemCount: items.length, + ); + } +} diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart new file mode 100644 index 0000000..fa052f9 --- /dev/null +++ b/lib/fragments/config.dart @@ -0,0 +1,188 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ConfigFragment extends StatefulWidget { + const ConfigFragment({super.key}); + + @override + State createState() => _ConfigFragmentState(); +} + +class _ConfigFragmentState extends State { + _modifyMixedPort(num mixedPort) async { + final port = await globalState.showCommonDialog( + child: MixedPortFormDialog( + mixedPort: mixedPort, + ), + ); + if (port != null && port != mixedPort && mounted) { + try { + final mixedPort = int.parse(port); + if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port"; + context.appController.clashConfig.mixedPort = mixedPort; + context.appController.updateClashConfigDebounce(); + } catch (e) { + globalState.showMessage( + title: appLocalizations.proxyPort, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + } + + _updateLoglevel(LogLevel? logLevel) { + if (logLevel == null || + logLevel == context.appController.clashConfig.logLevel) return; + context.appController.clashConfig.logLevel = logLevel; + context.appController.updateClashConfigDebounce(); + } + + @override + Widget build(BuildContext context) { + List items = [ + Selector( + selector: (_, clashConfig) => clashConfig.allowLan, + builder: (_, allowLan, __) { + return ListItem.switchItem( + leading: const Icon(Icons.device_hub), + title: Text(appLocalizations.allowLan), + subtitle: Text(appLocalizations.allowLanDesc), + delegate: SwitchDelegate( + value: allowLan, + onChanged: (bool value) async { + final clashConfig = context.read(); + clashConfig.allowLan = value; + context.appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.mixedPort, + builder: (_, mixedPort, __) { + return ListItem( + onTab: () { + _modifyMixedPort(mixedPort); + }, + leading: const Icon(Icons.adjust), + title: Text(appLocalizations.proxyPort), + trailing: FilledButton.tonal( + onPressed: () { + _modifyMixedPort(mixedPort); + }, + child: Text( + "$mixedPort", + ), + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.logLevel, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.feedback), + title: Text(appLocalizations.logLevel), + trailing: SizedBox( + height: 48, + child: DropdownMenu( + width: 124, + inputDecorationTheme: const InputDecorationTheme( + filled: true, + contentPadding: EdgeInsets.symmetric( + vertical: 5, + horizontal: 16, + ), + ), + initialSelection: value, + dropdownMenuEntries: [ + for (final logLevel in LogLevel.values) + DropdownMenuEntry( + value: logLevel, + label: logLevel.name, + ) + ], + onSelected: _updateLoglevel, + ), + ), + ); + }, + ), + ]; + return ListView.separated( + itemBuilder: (_, index) { + return Container( + height: 84, + alignment: Alignment.center, + child: items[index], + ); + }, + separatorBuilder: (_, __) { + return const Divider( + height: 0, + ); + }, + itemCount: items.length, + ); + } +} + +class MixedPortFormDialog extends StatefulWidget { + final num mixedPort; + + const MixedPortFormDialog({super.key, required this.mixedPort}); + + @override + State createState() => _MixedPortFormDialogState(); +} + +class _MixedPortFormDialogState extends State { + late TextEditingController portController; + + @override + void initState() { + super.initState(); + portController = TextEditingController(text: "${widget.mixedPort}"); + } + + _handleAddProfileFormURL() async { + final port = portController.value.text; + if (port.isEmpty) return; + Navigator.of(context).pop(port); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.proxyPort), + content: SizedBox( + width: 300, + child: Wrap( + runSpacing: 16, + children: [ + TextField( + controller: portController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _handleAddProfileFormURL, + child: Text(appLocalizations.submit), + ) + ], + ); + } +} diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart new file mode 100644 index 0000000..49b1932 --- /dev/null +++ b/lib/fragments/connections.dart @@ -0,0 +1,140 @@ +import 'dart:async'; + +import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/plugins/app.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class ConnectionsFragment extends StatefulWidget { + const ConnectionsFragment({super.key}); + + @override + State createState() => _ConnectionsFragmentState(); +} + +class _ConnectionsFragmentState extends State { + final connectionsNotifier = ValueNotifier>([]); + Map idPackageNameMap = {}; + + Timer? timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _getConnections(); + if (timer != null) { + timer?.cancel(); + timer = null; + } + timer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (mounted) { + _getConnections(); + } + }); + }); + } + + _getConnections() { + connectionsNotifier.value = clashCore + .getConnections(); + } + + @override + void dispose() { + super.dispose(); + timer?.cancel(); + timer = null; + } + + Future _getPackageIconWithConnection( + Connection connection) async { + final uid = connection.metadata.uid; + // if(globalState.packageNameMap[uid] == null){ + // globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata); + // } + final packageName = globalState.packageNameMap[uid]; + if(packageName == null) return null; + return await app?.getPackageIcon(packageName); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: connectionsNotifier, + builder: (_, List connections, __) { + if (connections.isEmpty) { + return const NullStatus( + label: "未开启代理,或者没有连接数据", + ); + } + return ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (_, index) { + final connection = connections[index]; + return ListTile( + titleAlignment: ListTileTitleAlignment.top, + leading: Container( + margin: const EdgeInsets.only(top: 4), + width: 48, + height: 48, + child: FutureBuilder( + future: _getPackageIconWithConnection(connection), + builder: (_, snapshot) { + if (!snapshot.hasData && snapshot.data == null) { + return Container(); + } else { + return Image( + image: snapshot.data!, + gaplessPlayback: true, + width: 48, + height: 48, + ); + } + }, + ), + ), + contentPadding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(connection.metadata.host.isNotEmpty + ? connection.metadata.host + : connection.metadata.destinationIP), + Padding( + padding: const EdgeInsets.only( + top: 12, + ), + child: Wrap( + runSpacing: 8, + spacing: 8, + children: [ + for (final chain in connection.chains) + CommonChip( + label: chain, + ), + ], + ), + ), + ], + ), + trailing: IconButton( + icon: const Icon(Icons.block), + onPressed: () {}, + ), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 0, + ); + }, + itemCount: connections.length, + ); + }, + ); + } +} diff --git a/lib/fragments/dashboard/core_info.dart b/lib/fragments/dashboard/core_info.dart new file mode 100644 index 0000000..f10303f --- /dev/null +++ b/lib/fragments/dashboard/core_info.dart @@ -0,0 +1,58 @@ +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CoreInfo extends StatelessWidget { + const CoreInfo({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.versionInfo, + builder: (_, versionInfo, __) { + debugPrint("[CoreInfo] update===>"); + return CommonCard( + info: Info( + label: appLocalizations.coreInfo, + iconData: Icons.memory, + ), + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(16).copyWith(top: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + flex: 1, + child: Text( + versionInfo?.clashName ?? '', + style: context + .textTheme + .titleMedium + ?.toSoftBold(), + ), + ), + const SizedBox( + height: 8, + ), + Flexible( + flex: 1, + child: Text( + versionInfo?.version ?? '', + style: context + .textTheme + .titleLarge + ?.toSoftBold(), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart new file mode 100644 index 0000000..5c3646d --- /dev/null +++ b/lib/fragments/dashboard/dashboard.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:fl_clash/widgets/widgets.dart'; + +import 'network_detection.dart'; +import 'core_info.dart'; +import 'outbound_mode.dart'; +import 'start_button.dart'; +import 'network_speed.dart'; +import 'traffic_usage.dart'; + +class DashboardFragment extends StatefulWidget { + const DashboardFragment({super.key}); + + @override + State createState() => _DashboardFragmentState(); +} + +class _DashboardFragmentState extends State { + _buildGrid(bool isDesktop) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Grid( + crossAxisCount: 12, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + GridItem( + crossAxisCellCount: isDesktop ? 8 : 12, + child: const NetworkSpeed(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const OutboundMode(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const NetworkDetection(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const TrafficUsage(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const CoreInfo(), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, container) { + if (container.maxWidth < 200) return Container(); + return FloatLayout( + floatingWidget: const FloatWrapper( + child: StartButton(), + ), + child: Align( + alignment: Alignment.topCenter, + child: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('dashboard_small'), + builder: (_) => _buildGrid(false), + ), + Breakpoints.mediumAndUp: SlotLayout.from( + key: const Key('dashboard_mediumAndUp'), + builder: (_) => _buildGrid(true), + ), + }, + ), + ), + ); + }); + } +} diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart new file mode 100644 index 0000000..9bb0f8f --- /dev/null +++ b/lib/fragments/dashboard/network_detection.dart @@ -0,0 +1,190 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NetworkDetection extends StatefulWidget { + const NetworkDetection({super.key}); + + @override + State createState() => _NetworkDetectionState(); +} + +class _NetworkDetectionState extends State { + Widget _buildDescription(String? currentProxyName, int? delay) { + if (currentProxyName == null) { + return TooltipText( + text: Text( + appLocalizations.noProxyDesc, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + overflow: TextOverflow.ellipsis, + ), + ); + } + if (delay == 0 || delay == null) { + return const AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator( + strokeCap: StrokeCap.round, + ), + ); + } + if (delay > 0) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TooltipText( + text: Text( + "$delay", + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: context.textTheme.titleLarge + ?.copyWith( + color: context.colorScheme.primary, + ) + .toSoftBold(), + ), + ), + const Flexible( + child: SizedBox( + width: 4, + ), + ), + Flexible( + flex: 0, + child: Text( + 'ms', + style: Theme.of(context).textTheme.bodyMedium?.toLight(), + ), + ), + ], + ); + } + return Text( + "Timeout", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.red, + ), + ); + } + + _updateCurrentDelay( + String? currentProxyName, + int? delay, + bool isCurrent, + bool isInit, + ) { + if (!isCurrent || currentProxyName == null || !isInit) return; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (delay == null) { + context.appController.setDelay( + Delay( + name: currentProxyName, + value: 0, + ), + ); + globalState.updateCurrentDelay( + currentProxyName, + ); + } + }); + } + + _updateCurrentDelayContainer(Widget child) { + return Selector3( + selector: (_, appState, config, clashConfig) { + final proxyName = appState.getCurrentProxyName( + config.currentProxyName, + clashConfig.mode, + ); + return UpdateCurrentDelaySelectorState( + isInit: appState.isInit, + currentProxyName: proxyName, + delay: appState.delayMap[proxyName], + isCurrent: appState.currentLabel == 'dashboard', + ); + }, + builder: (_, state, __) { + debugPrint("[UpdateCurrentDelay] update===>"); + _updateCurrentDelay( + state.currentProxyName, + state.delay, + state.isCurrent, + state.isInit, + ); + return child; + }, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return CommonCard( + info: Info( + iconData: Icons.network_check, + label: appLocalizations.networkDetection, + ), + child: _updateCurrentDelayContainer( + Selector3( + selector: (_, appState, config, clashConfig) { + final proxyName = appState.getCurrentProxyName( + config.currentProxyName, + clashConfig.mode, + ); + return NetworkDetectionSelectorState( + isInit: appState.isInit, + currentProxyName: proxyName, + delay: appState.delayMap[proxyName], + ); + }, + builder: (_, state, __) { + debugPrint("[NetworkDetection] update===>"); + return Container( + padding: const EdgeInsets.all(16).copyWith(top: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 0, + child: TooltipText( + text: Text( + state.currentProxyName ?? appLocalizations.noProxy, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: + Theme.of(context).textTheme.titleMedium?.toSoftBold(), + ), + ), + ), + const SizedBox( + height: 8, + ), + Flexible( + child: Container( + height: context.appController.measure.titleLargeHeight, + alignment: Alignment.centerLeft, + child: FadeBox( + child: _buildDescription( + state.currentProxyName, + state.delay, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/network_speed.dart b/lib/fragments/dashboard/network_speed.dart new file mode 100644 index 0000000..76ba1dc --- /dev/null +++ b/lib/fragments/dashboard/network_speed.dart @@ -0,0 +1,174 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NetworkSpeed extends StatefulWidget { + const NetworkSpeed({super.key}); + + @override + State createState() => _NetworkSpeedState(); +} + +class _NetworkSpeedState extends State { + List initPoints = const [Point(0, 0), Point(1, 0)]; + + List _getPoints(List traffics) { + List trafficPoints = traffics + .toList() + .asMap() + .map( + (index, e) => MapEntry( + index, + Point( + (index + initPoints.length).toDouble(), + e.speed.toDouble(), + ), + ), + ) + .values + .toList(); + var pointsRaw = [...initPoints, ...trafficPoints]; + List points; + if (pointsRaw.length > 60) { + points = pointsRaw + .getRange(pointsRaw.length - 61, pointsRaw.length - 1) + .toList(); + } else { + points = pointsRaw; + } + + return points; + } + + Traffic _getLastTraffic(List traffics) { + if (traffics.isEmpty) return Traffic(); + return traffics.last; + } + + Widget _getLabel({ + required String label, + required IconData iconData, + required TrafficValue value, + }) { + + final showValue = value.showValue; + final showUnit = "${value.showUnit}/s"; + final titleLargeSoftBold = + Theme.of(context).textTheme.titleLarge?.toSoftBold(); + final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight(); + final valueText = Text( + showValue, + style: titleLargeSoftBold, + maxLines: 1, + ); + final unitText = Text( + showUnit, + style: bodyMedium, + maxLines: 1, + ); + final size = context.appController.measure.computeTextSize(valueText); + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Icon(iconData), + ), + Flexible( + child: Text( + label, + style: Theme.of(context).textTheme.titleSmall?.toSoftBold(), + ), + ), + ], + ), + SizedBox( + width: size.width, + height: size.height, + child: OverflowBox( + maxWidth: 156, + alignment: Alignment.centerLeft, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: valueText, + ), + const Flexible( + flex: 0, + child: SizedBox( + width: 4, + ), + ), + Flexible( + child: unitText, + ), + ], + ), + )) + ], + ); + } + + @override + Widget build(BuildContext context) { + return CommonCard( + info: Info( + label: appLocalizations.networkSpeed, + iconData: Icons.speed, + ), + child: Selector>( + selector: (_, appState) => appState.traffics, + builder: (_, traffics, __) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 0, + child: LineChart( + color: Theme.of(context).colorScheme.primary, + points: _getPoints(traffics), + height: 100, + ), + ), + const Flexible(child: SizedBox(height: 16)), + Flexible( + flex: 0, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: _getLabel( + iconData: Icons.upload, + label: appLocalizations.upload, + value: _getLastTraffic(traffics).up, + ), + ), + Expanded( + child: _getLabel( + iconData: Icons.download, + label: appLocalizations.download, + value: _getLastTraffic(traffics).down, + ), + ), + ], + ), + ) + ], + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/fragments/dashboard/outbound_mode.dart b/lib/fragments/dashboard/outbound_mode.dart new file mode 100644 index 0000000..e87f6bb --- /dev/null +++ b/lib/fragments/dashboard/outbound_mode.dart @@ -0,0 +1,70 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class OutboundMode extends StatelessWidget { + const OutboundMode({super.key}); + + _changeMode(BuildContext context, Mode? value) async { + final appController = context.appController; + final clashConfig = context.read(); + if (value == null || clashConfig.mode == value) return; + clashConfig.mode = value; + await appController.updateClashConfig(); + appController.changeProxy(); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.mode, + builder: (_, mode, __) { + return CommonCard( + info: Info( + label: appLocalizations.outboundMode, + iconData: Icons.call_split, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (final item in Mode.values) + ListItem.radio( + horizontalTitleGap: 4, + prue: true, + padding: const EdgeInsets.only( + left: 12, + right: 16, + top: 8, + bottom: 8, + ), + delegate: RadioDelegate( + value: item, + groupValue: mode, + onChanged: (value) async { + _changeMode(context, value); + }, + ), + title: Text( + Intl.message(item.name), + style: Theme.of(context) + .textTheme + .titleMedium + ?.toSoftBold(), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/fragments/dashboard/start_button.dart b/lib/fragments/dashboard/start_button.dart new file mode 100644 index 0000000..b88ed7a --- /dev/null +++ b/lib/fragments/dashboard/start_button.dart @@ -0,0 +1,144 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class StartButton extends StatefulWidget { + const StartButton({super.key}); + + @override + State createState() => _StartButtonState(); +} + +class _StartButtonState extends State + with SingleTickerProviderStateMixin { + bool isStart = false; + bool isInit = false; + late AnimationController _controller; + + @override + void initState() { + isStart = context.read().runTime != null; + _controller = AnimationController( + vsync: this, + value: isStart ? 1 : 0, + duration: const Duration(milliseconds: 200), + ); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + handleSwitchStart() { + isStart = !isStart; + updateController(); + updateSystemProxy(); + } + + updateController() { + if (isStart) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + updateSystemProxy() async { + final appController = context.appController; + await appController.updateSystemProxy(isStart); + if (isStart && mounted) { + appController.clearCurrentDelay(); + } + } + + @override + Widget build(BuildContext context) { + return Selector2( + selector: (_, appState, config) => StartButtonSelectorState( + isInit: appState.isInit, + hasProfile: config.profiles.isNotEmpty, + ), + builder: (_, state, child) { + debugPrint("[StartButton] update===>"); + if (!state.isInit || !state.hasProfile) { + return Container(); + } + final textWidth = context.appController.measure.computeTextSize( + Text( + Other.getTimeDifference( + DateTime.now(), + ), + style: Theme.of(context).textTheme.titleMedium?.toSoftBold(), + ), + ).width + + 16; + return AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return SizedBox( + width: 56 + textWidth * _controller.value, + height: 56, + child: FloatingActionButton( + heroTag: null, + onPressed: () { + handleSwitchStart(); + }, + child: Row( + children: [ + Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: _controller, + ), + ), + Expanded( + child: ClipRect( + child: OverflowBox( + maxWidth: textWidth, + child: Container( + alignment: Alignment.centerLeft, + child: child!, + ), + ), + ), + ), + ], + ), + ), + ); + }, + child: child, + ); + }, + child: Selector( + selector: (_, appState) => appState.runTime != null, + builder: (_, isRun, child) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isStart != isRun) { + isStart = isRun; + updateController(); + } + }); + return child!; + }, + child: Selector( + selector: (_, appState) => appState.runTime, + builder: (_, int? value, __) { + final text = Other.getTimeText(value); + return Text( + text, + style: Theme.of(context).textTheme.titleMedium?.toSoftBold(), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/traffic_usage.dart b/lib/fragments/dashboard/traffic_usage.dart new file mode 100644 index 0000000..a50bdbc --- /dev/null +++ b/lib/fragments/dashboard/traffic_usage.dart @@ -0,0 +1,104 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TrafficUsage extends StatelessWidget { + const TrafficUsage({super.key}); + + Widget getTrafficDataItem( + BuildContext context, + IconData iconData, + TrafficValue trafficValue, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + iconData, + size: 18, + ), + const SizedBox( + width: 8, + ), + Flexible( + flex: 1, + child: Text( + trafficValue.showValue, + style: context.textTheme.labelLarge?.copyWith(fontSize: 18), + maxLines: 1, + ), + ), + ], + ), + ), + Text( + trafficValue.showUnit, + style: context.textTheme.labelMedium?.toLight(), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return CommonCard( + info: Info( + label: appLocalizations.trafficUsage, + iconData: Icons.data_saver_off, + ), + child: Selector>( + selector: (_, appState) => appState.traffics, + builder: (_, traffics, __) { + final trafficTotal = traffics.isNotEmpty + ? traffics.reduce( + (value, element) { + return Traffic( + up: element.up.value + value.up.value, + down: element.down.value + value.down.value, + ); + }, + ) + : Traffic(); + final upTrafficValue = trafficTotal.up; + final downTrafficValue = trafficTotal.down; + return Padding( + padding: const EdgeInsets.all(16).copyWith(top: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + flex: 1, + child: getTrafficDataItem( + context, + Icons.arrow_upward, + upTrafficValue, + ), + ), + const SizedBox( + height: 4, + ), + Flexible( + flex: 1, + child: getTrafficDataItem( + context, + Icons.arrow_downward, + downTrafficValue, + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/fragments/fragments.dart b/lib/fragments/fragments.dart new file mode 100644 index 0000000..20640a5 --- /dev/null +++ b/lib/fragments/fragments.dart @@ -0,0 +1,10 @@ +export 'proxies.dart'; +export 'dashboard/dashboard.dart'; +export 'tools.dart'; +export 'profiles/profiles.dart'; +export 'logs.dart'; +export 'connections.dart'; +export 'access.dart'; +export 'config.dart'; +export 'application_setting.dart'; +export 'about.dart'; \ No newline at end of file diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart new file mode 100644 index 0000000..42ed6ff --- /dev/null +++ b/lib/fragments/logs.dart @@ -0,0 +1,214 @@ +import 'dart:async'; + +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/models.dart'; +import '../widgets/widgets.dart'; + +class LogsFragment extends StatefulWidget { + const LogsFragment({super.key}); + + @override + State createState() => _LogsFragmentState(); +} + +class _LogsFragmentState extends State { + final logsNotifier = ValueNotifier>([]); + Timer? timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + logsNotifier.value = context.read().logs; + if (timer != null) { + timer?.cancel(); + timer = null; + } + timer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (mounted) { + logsNotifier.value = context.read().logs; + } + }); + }); + } + + @override + void dispose() { + super.dispose(); + timer?.cancel(); + timer = null; + } + + _initActions() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + showSearch( + context: context, + delegate: LogsSearchDelegate( + logs: logsNotifier.value.reversed.toList(), + ), + ); + }, + icon: const Icon(Icons.search), + ) + ]; + }); + } + + _buildList() { + return ValueListenableBuilder>( + valueListenable: logsNotifier, + builder: (_, List logs, __) { + if (logs.isEmpty) { + return NullStatus( + label: appLocalizations.nullLogsDesc, + ); + } + logs = logs.reversed.toList(); + return ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: logs.length, + itemBuilder: (BuildContext context, int index) { + final log = logs[index]; + return LogItem( + log: log, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 0, + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => + appState.currentLabel == 'logs' || + context.isMobile && appState.currentLabel == "tools", + builder: (_, isCurrent, child) { + if (isCurrent == null || isCurrent) { + _initActions(); + } + return child!; + }, + child: _buildList(), + ); + } +} + +class LogsSearchDelegate extends SearchDelegate { + List logs = []; + + LogsSearchDelegate({ + required this.logs, + }); + + List get _results => logs + .where( + (log) => + (log.payload?.contains(query) ?? false) || + log.logLevel.name.contains(query), + ) + .toList(); + + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + onPressed: () { + if (query.isEmpty) { + close(context, null); + return; + } + query = ''; + }, + icon: const Icon(Icons.clear), + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: () { + close(context, null); + }, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + return buildSuggestions(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _results.length, + itemBuilder: (BuildContext context, int index) { + final log = _results[index]; + return LogItem( + log: log, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 0, + ); + }, + ); + } +} + +class LogItem extends StatelessWidget { + final Log log; + + const LogItem({ + super.key, + required this.log, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: SelectableText(log.payload ?? ''), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8, + ), + child: SelectableText( + "${log.dateTime}", + style: context.textTheme.bodySmall + ?.copyWith(color: context.colorScheme.primary), + ), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: CommonChip( + label: log.logLevel.name, + ), + ), + ], + ), + ); + } +} diff --git a/lib/fragments/profiles/add_profile.dart b/lib/fragments/profiles/add_profile.dart new file mode 100644 index 0000000..a033c53 --- /dev/null +++ b/lib/fragments/profiles/add_profile.dart @@ -0,0 +1,109 @@ +import 'dart:io'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/pages/scan.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class AddProfile extends StatelessWidget { + final BuildContext context; + + const AddProfile({super.key, required this.context}); + + _handleAddProfileFormFile() async { + context.appController.addProfileFormFile(); + } + + _handleAddProfileFormURL(String url) async { + context.appController.addProfileFormURL(url); + } + + _toScan() async { + final url = await Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => const ScanPage())); + if (url != null) { + _handleAddProfileFormURL(url); + } + } + + _toAdd() async { + final url = await globalState.showCommonDialog( + child: const URLFormDialog(), + ); + if (url != null) { + _handleAddProfileFormURL(url); + } + } + + @override + Widget build(context) { + return ListView( + children: [ + if (Platform.isAndroid) + ListItem( + leading: const Icon(Icons.qr_code), + title: Text(appLocalizations.qrcode), + subtitle: Text(appLocalizations.qrcodeDesc), + onTab: _toScan, + ), + ListItem( + leading: const Icon(Icons.upload_file), + title: Text(appLocalizations.file), + subtitle: Text(appLocalizations.fileDesc), + onTab: _handleAddProfileFormFile, + ), + ListItem( + leading: const Icon(Icons.cloud_download), + title: Text(appLocalizations.url), + subtitle: Text(appLocalizations.urlDesc), + onTab: _toAdd, + ) + ], + ); + } +} + +class URLFormDialog extends StatefulWidget { + const URLFormDialog({super.key}); + + @override + State createState() => _URLFormDialogState(); +} + +class _URLFormDialogState extends State { + final urlController = TextEditingController(); + + _handleAddProfileFormURL() async { + final url = urlController.value.text; + if (url.isEmpty) return; + Navigator.of(context).pop(url); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.importFromURL), + content: SizedBox( + width: 300, + child: Wrap( + runSpacing: 16, + children: [ + TextField( + controller: urlController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: appLocalizations.url, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _handleAddProfileFormURL, + child: Text(appLocalizations.submit), + ) + ], + ); + } +} diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart new file mode 100644 index 0000000..f753097 --- /dev/null +++ b/lib/fragments/profiles/edit_profile.dart @@ -0,0 +1,166 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EditProfile extends StatefulWidget { + final Profile profile; + final BuildContext context; + + const EditProfile({ + super.key, + required this.context, + required this.profile, + }); + + @override + State createState() => _EditProfileState(); +} + +class _EditProfileState extends State { + late TextEditingController labelController; + late TextEditingController urlController; + late TextEditingController autoUpdateDurationController; + late bool autoUpdate; + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + labelController = TextEditingController(text: widget.profile.label); + urlController = TextEditingController(text: widget.profile.url); + autoUpdate = widget.profile.autoUpdate; + autoUpdateDurationController = TextEditingController( + text: widget.profile.autoUpdateDuration.inMinutes.toString(), + ); + } + + _handleConfirm() { + if (!_formKey.currentState!.validate()) return; + final config = context.read(); + final hasUpdate = widget.profile.url != urlController.text; + widget.profile.url = urlController.text; + widget.profile.label = labelController.text; + widget.profile.autoUpdate = autoUpdate; + widget.profile.autoUpdateDuration = + Duration(minutes: int.parse(autoUpdateDurationController.text)); + config.setProfile(widget.profile); + if (hasUpdate) { + widget.context.findAncestorStateOfType()?.loadingRun( + () => context.appController.updateProfile( + widget.profile.id, + ), + ); + } + Navigator.of(context).pop(); + } + + _setAutoUpdate(bool value) { + if (autoUpdate == value) return; + setState(() { + autoUpdate = value; + }); + } + + @override + Widget build(BuildContext context) { + final items = [ + ListItem( + title: TextFormField( + controller: labelController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: appLocalizations.name, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.profileNameNullValidationDesc; + } + return null; + }, + ), + ), + if (widget.profile.url != null)...[ + ListItem( + title: TextFormField( + controller: urlController, + maxLines: 2, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: appLocalizations.url, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.profileUrlNullValidationDesc; + } + if (!value.isUrl) { + return appLocalizations.profileUrlInvalidValidationDesc; + } + return null; + }, + ), + ), + ListItem.switchItem( + title: Text(appLocalizations.autoUpdate), + delegate: SwitchDelegate( + value: autoUpdate, + onChanged: _setAutoUpdate, + ), + ), + if (autoUpdate) + ListItem( + title: TextFormField( + controller: autoUpdateDurationController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: appLocalizations.autoUpdateInterval, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations + .profileAutoUpdateIntervalNullValidationDesc; + } + try { + int.parse(value); + } catch (_) { + return appLocalizations + .profileAutoUpdateIntervalInvalidValidationDesc; + } + return null; + }, + ), + ), + ] + ]; + return FloatLayout( + floatingWidget: FloatWrapper( + child: FloatingActionButton.extended( + heroTag: null, + onPressed: _handleConfirm, + label: Text(appLocalizations.save), + icon: const Icon(Icons.save), + ), + ), + child: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + child: ListView.separated( + itemBuilder: (_, index) { + return items[index]; + }, + separatorBuilder: (_, __) { + return const SizedBox( + height: 24, + ); + }, + itemCount: items.length, + ), + ), + ), + ); + } +} diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart new file mode 100644 index 0000000..3c978e8 --- /dev/null +++ b/lib/fragments/profiles/profiles.dart @@ -0,0 +1,275 @@ +import 'package:fl_clash/fragments/profiles/edit_profile.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:provider/provider.dart'; + +import 'add_profile.dart'; + +enum PopupMenuItemEnum { delete, edit } + +enum ProfileActions { + edit, + update, + delete, +} + +class ProfilesFragment extends StatefulWidget { + const ProfilesFragment({super.key}); + + @override + State createState() => _ProfilesFragmentState(); +} + +class _ProfilesFragmentState extends State { + String _getLastUpdateTimeDifference(DateTime lastDateTime) { + final currentDateTime = DateTime.now(); + final difference = currentDateTime.difference(lastDateTime); + final days = difference.inDays; + if (days >= 365) { + return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}"; + } + if (days >= 30) { + return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}"; + } + if (days >= 1) { + return "$days ${appLocalizations.days}${appLocalizations.ago}"; + } + final hours = difference.inHours; + if (hours >= 1) { + return "$hours ${appLocalizations.hours}${appLocalizations.ago}"; + } + final minutes = difference.inMinutes; + if (minutes >= 1) { + return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}"; + } + return appLocalizations.just; + } + + _handleDeleteProfile(String id) async { + context.appController.deleteProfile(id); + } + + _handleUpdateProfile(String id) async { + context.findAncestorStateOfType()?.loadingRun( + () => context.appController.updateProfile(id), + ); + } + + Widget _profileItem({ + required Profile profile, + required String? groupValue, + required void Function(String? value) onChanged, + }) { + String useShow; + String totalShow; + double progress; + final userInfo = profile.userInfo; + if (userInfo == null) { + useShow = "Infinite"; + totalShow = "Infinite"; + progress = 1; + } else { + final use = userInfo.upload + userInfo.download; + final total = userInfo.total; + useShow = TrafficValue(value: use).show; + totalShow = TrafficValue(value: total).show; + progress = total == 0 ? 0.0 : use / total; + } + return ListItem.radio( + horizontalTitleGap: 16, + delegate: RadioDelegate( + value: profile.id, + groupValue: groupValue, + onChanged: onChanged, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + trailing: CommonPopupMenu( + items: [ + CommonPopupMenuItem( + action: ProfileActions.edit, + label: appLocalizations.edit, + iconData: Icons.edit, + ), + if (profile.url != null) + CommonPopupMenuItem( + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, + ), + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(profile); + break; + case ProfileActions.delete: + _handleDeleteProfile(profile.id); + break; + case ProfileActions.update: + _handleUpdateProfile(profile.id); + break; + case null: + break; + } + }, + ), + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + profile.label ?? profile.id, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + profile.lastUpdateDate != null + ? _getLastUpdateTimeDifference(profile.lastUpdateDate!) + : '', + style: Theme.of(context).textTheme.labelMedium?.toLight(), + ), + ), + ], + ), + ), + Flexible( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 8, + ), + child: LinearProgressIndicator( + minHeight: 6, + value: progress, + ), + ), + ), + Flexible( + child: Text( + "$useShow / $totalShow", + style: Theme.of(context).textTheme.labelMedium?.toLight(), + ), + ), + ], + ), + ); + } + + _handleShowAddExtendPage() { + showExtendPage( + context, + body: AddProfile( + context: context, + ), + title: "${appLocalizations.add}${appLocalizations.profile}", + ); + } + + _handleShowEditExtendPage(Profile profile) { + showExtendPage( + context, + body: EditProfile( + profile: profile.copyWith(), + context: context, + ), + title: "${appLocalizations.edit}${appLocalizations.profile}", + ); + } + + _buildGrid({ + required ProfilesSelectorState state, + int crossAxisCount = 1, + }) { + return SingleChildScrollView( + padding: crossAxisCount > 1 + ? const EdgeInsets.symmetric(horizontal: 16) + : EdgeInsets.zero, + child: Grid.baseGap( + crossAxisCount: crossAxisCount, + children: [ + for (final profile in state.profiles) + GridItem( + child: _profileItem( + profile: profile, + groupValue: state.currentProfileId, + onChanged: context.appController.changeProfile, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return FloatLayout( + floatingWidget: Container( + margin: const EdgeInsets.all(kFloatingActionButtonMargin), + child: FloatingActionButton( + heroTag: null, + onPressed: _handleShowAddExtendPage, + child: const Icon(Icons.add), + ), + ), + child: Selector( + selector: (_, config) => ProfilesSelectorState( + profiles: config.profiles, + currentProfileId: config.currentProfileId, + ), + builder: (context, state, child) { + debugPrint("[Profiles] update===>"); + if (state.profiles.isEmpty) { + return NullStatus( + label: appLocalizations.nullProfileDesc, + ); + } + return Align( + alignment: Alignment.topCenter, + child: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('profiles_grid_small'), + builder: (_) => _buildGrid( + state: state, + crossAxisCount: 1, + ), + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('profiles_grid_medium'), + builder: (_) => _buildGrid( + state: state, + crossAxisCount: 1, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('profiles_grid_large'), + builder: (_) => _buildGrid( + state: state, + crossAxisCount: 2, + ), + ), + }, + ), + ); + }, + ), + ); + } +} diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart new file mode 100644 index 0000000..8d57616 --- /dev/null +++ b/lib/fragments/proxies.dart @@ -0,0 +1,469 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/clash/clash.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:provider/provider.dart'; + +import '../enum/enum.dart'; +import '../models/models.dart'; +import '../common/common.dart'; +import '../widgets/widgets.dart'; + +class ProxiesFragment extends StatefulWidget { + const ProxiesFragment({super.key}); + + @override + State createState() => _ProxiesFragmentState(); +} + +class _ProxiesFragmentState extends State + with TickerProviderStateMixin { + TabController? _tabController; + + _initActions() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final commonScaffoldState = + context.findAncestorStateOfType(); + final items = [ + CommonPopupMenuItem( + action: ProxiesSortType.none, + label: appLocalizations.defaultSort, + iconData: Icons.sort, + ), + CommonPopupMenuItem( + action: ProxiesSortType.delay, + label: appLocalizations.delaySort, + iconData: Icons.network_ping), + CommonPopupMenuItem( + action: ProxiesSortType.name, + label: appLocalizations.nameSort, + iconData: Icons.sort_by_alpha), + ]; + commonScaffoldState?.actions = [ + Selector( + selector: (_, config) => config.proxiesSortType, + builder: (_, proxiesSortType, __) { + return CommonPopupMenu.radio( + items: items, + onSelected: (value) { + final config = context.read(); + config.proxiesSortType = value; + }, + selectedValue: proxiesSortType, + ); + }, + ) + ]; + }); + } + + _updateTabController(length) { + if (_tabController != null) { + _tabController!.dispose(); + _tabController = null; + } + _tabController = TabController( + length: length, + vsync: this, + ); + } + + @override + void dispose() { + if (_tabController != null) { + _tabController!.dispose(); + _tabController = null; + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.currentLabel == 'proxies', + builder: (_, isCurrent, child) { + if (isCurrent) { + _initActions(); + } + return child!; + }, + child: Selector2>( + selector: (_, appState, clashConfig) => + appState.getCurrentGroups(clashConfig.mode), + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, groups, __) { + debugPrint("[Proxies] update===>"); + _updateTabController(groups.length); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + controller: _tabController, + padding: const EdgeInsets.symmetric(horizontal: 16), + dividerColor: Colors.transparent, + isScrollable: true, + tabAlignment: TabAlignment.start, + overlayColor: + const MaterialStatePropertyAll(Colors.transparent), + tabs: [ + for (final group in groups) + Tab( + text: group.name, + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final group in groups) + KeepContainer( + child: ProxiesTabView( + group: group, + ), + ), + ], + ), + ) + ], + ); + }, + ), + ); + } +} + +class ProxiesTabView extends StatefulWidget { + final Group group; + + const ProxiesTabView({ + super.key, + required this.group, + }); + + @override + State createState() => _ProxiesTabViewState(); +} + +class _ProxiesTabViewState extends State + with SingleTickerProviderStateMixin { + var lock = false; + late AnimationController _controller; + late Animation _scale; + late Animation _opacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: 200, + ), + ); + _scale = Tween( + begin: 1.0, + end: 0.8, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0.0, + 0.3, + curve: Curves.easeIn, + ), + ), + ); + _opacity = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0, + 1, + curve: Curves.easeIn, + ), + ), + ); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + get group => widget.group; + + get measure => context.appController.measure; + + List _sortOfName(List proxies) { + return List.of(proxies) + ..sort( + (a, b) => Other.sortByChar(a.name, b.name), + ); + } + + List _sortOfDelay(List proxies) { + final appState = context.read(); + final delayMap = appState.delayMap; + return proxies = List.of(proxies) + ..sort( + (a, b) { + final aDelay = delayMap[a.name]; + final bDelay = delayMap[b.name]; + if (aDelay == null && bDelay == null) { + return 0; + } + if (aDelay == null || aDelay == -1) { + return 1; + } + if (bDelay == null || bDelay == -1) { + return -1; + } + return aDelay.compareTo(bDelay); + }, + ); + } + + _getProxies( + List proxies, + ProxiesSortType proxiesSortType, + ) { + if (proxiesSortType == ProxiesSortType.delay) return _sortOfDelay(proxies); + if (proxiesSortType == ProxiesSortType.name) return _sortOfName(proxies); + return proxies; + } + + _getDelayMap() async { + if (lock == true) return; + lock = true; + _controller.forward(); + for (final proxy in group.all) { + context.appController.setDelay( + Delay( + name: proxy.name, + value: 0, + ), + ); + clashCore.delay( + proxy.name, + ); + } + await Future.delayed( + appConstant.httpTimeoutDuration + appConstant.moreDuration, + ); + lock = false; + _controller.reverse(); + setState(() {}); + } + + double _getItemHeight() { + return 12 * 2 + + measure.bodyMediumHeight * 2 + + measure.bodySmallHeight + + measure.labelSmallHeight + + 8 * 2; + } + + _card({ + required void Function() onPressed, + required bool isSelected, + required Proxy proxy, + }) { + return CommonCard( + isSelected: isSelected, + onPressed: onPressed, + selectWidget: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.all(8), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: const SelectIcon(), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: measure.bodyMediumHeight * 2, + child: Text( + proxy.name, + maxLines: 2, + style: context.textTheme.bodyMedium?.copyWith( + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + height: measure.bodySmallHeight, + child: Text( + proxy.type, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + height: measure.labelSmallHeight, + child: Selector( + selector: (context, appState) => appState.delayMap[proxy.name], + builder: (_, delay, __) { + return FadeBox( + child: Builder( + builder: (_) { + if (delay == null) { + return Container(); + } + if (delay == 0) { + return SizedBox( + height: measure.labelSmallHeight, + width: measure.labelSmallHeight, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ); + } + return Text( + delay > 0 ? '$delay ms' : "Timeout", + style: context.textTheme.labelSmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: Other.getDelayColor( + delay, + ), + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + _buildGrid({ + required ProxiesSortType proxiesSortType, + required int columns, + }) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: AnimateGrid( + items: _getProxies(group.all, proxiesSortType), + columns: columns, + itemHeight: _getItemHeight(), + keyBuilder: (item) { + return ObjectKey(item); + }, + builder: (_, proxy) { + return Selector3( + selector: (_, appState, config, clashConfig) => + appState.getCurrentProxyName( + config.currentProxyName, + clashConfig.mode, + ), + builder: (_, value, __) { + final currentProxyName = + group.type == GroupType.Selector ? value : group.now; + return _card( + isSelected: proxy.name == currentProxyName, + onPressed: () { + if (group.type == GroupType.Selector) { + final config = context.read(); + config.currentProfile?.groupName = group.name; + config.currentProfile?.proxyName = proxy.name; + config.update(); + context.appController.changeProxy(); + } + }, + proxy: proxy, + ); + }, + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.proxiesSortType, + builder: (_, proxiesSortType, __) { + return FloatLayout( + floatingWidget: FloatWrapper( + child: AnimatedBuilder( + animation: _controller, + builder: (_, __) { + return Transform.scale( + scale: _scale.value, + child: SizedBox( + width: 56, + height: 56, + child: Opacity( + opacity: _opacity.value, + child: FloatingActionButton( + heroTag: null, + onPressed: _getDelayMap, + child: const Icon(Icons.network_ping), + ), + ), + ), + ); + }, + ), + ), + child: Align( + alignment: Alignment.topCenter, + child: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('proxies_grid_small'), + builder: (_) => _buildGrid( + proxiesSortType: proxiesSortType, + columns: 2, + ), + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('proxies_grid_medium'), + builder: (_) => _buildGrid( + proxiesSortType: proxiesSortType, + columns: 3, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('proxies_grid_large'), + builder: (_) => _buildGrid( + proxiesSortType: proxiesSortType, + columns: 4, + ), + ), + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart new file mode 100644 index 0000000..534c38f --- /dev/null +++ b/lib/fragments/theme.dart @@ -0,0 +1,218 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../widgets/widgets.dart'; + +class ThemeModeItem { + final ThemeMode themeMode; + final IconData iconData; + final String label; + + const ThemeModeItem({ + required this.themeMode, + required this.iconData, + required this.label, + }); +} + +class ThemeFragment extends StatelessWidget { + const ThemeFragment({super.key}); + + Widget _themeModeCheckBox({ + required BuildContext context, + bool? isSelected, + required ThemeModeItem themeModeItem, + }) { + return CommonCard( + isSelected: isSelected, + onPressed: () { + context.appController.config.themeMode = themeModeItem.themeMode; + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal:16), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: Icon(themeModeItem.iconData), + ), + const SizedBox( + width: 8, + ), + Flexible( + child: Text( + themeModeItem.label, + ), + ), + ], + ), + ), + ); + } + + Widget _primaryColorCheckBox({ + required BuildContext context, + bool? isSelected, + Color? color, + }) { + return ColorSchemeBox( + isSelected: isSelected, + primaryColor: color, + onPressed: () { + context.appController.config.primaryColor = color?.value; + }, + ); + } + + Widget _itemCard({ + required BuildContext context, + required Info info, + required Widget child, + }) { + return Padding( + padding: const EdgeInsets.only( + top: 16, + ), + child: Wrap( + runSpacing: 16, + children: [ + InfoHeader( + info: info, + ), + child, + ], + ), + ); + } + + Widget _getThemeCard(BuildContext context) { + List themeModeItems = [ + ThemeModeItem( + iconData: Icons.auto_mode, + label: appLocalizations.auto, + themeMode: ThemeMode.system, + ), + ThemeModeItem( + iconData: Icons.light_mode, + label: appLocalizations.light, + themeMode: ThemeMode.light, + ), + ThemeModeItem( + iconData: Icons.dark_mode, + label: appLocalizations.dark, + themeMode: ThemeMode.dark, + ), + ]; + List primaryColors = [ + null, + appConstant.defaultPrimaryColor, + Colors.pinkAccent, + Colors.greenAccent, + Colors.yellowAccent, + Colors.purple + ]; + return Column( + children: [ + _itemCard( + context: context, + info: Info( + label: appLocalizations.themeMode, + iconData: Icons.brightness_high, + ), + child: Selector( + selector: (_, config) => config.themeMode, + builder: (_, themeMode, __) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + height: 64, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: themeModeItems.length, + itemBuilder: (_, index) { + final themeModeItem = themeModeItems[index]; + return _themeModeCheckBox( + isSelected: themeMode == themeModeItem.themeMode, + context: context, + themeModeItem: themeModeItem, + ); + }, + separatorBuilder: (_, __) { + return const SizedBox( + width: 16, + ); + }, + ), + ); + }, + ), + ), + _itemCard( + context: context, + info: Info( + label: appLocalizations.themeColor, + iconData: Icons.palette, + ), + child: Container( + margin: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 16, + ), + height: 88, + child: Selector( + selector: (_, config) => config.primaryColor, + builder: (_, currentPrimaryColor, __) { + return ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) { + final primaryColor = primaryColors[index]; + return _primaryColorCheckBox( + context: context, + isSelected: currentPrimaryColor == primaryColor?.value, + color: primaryColor, + ); + }, + separatorBuilder: (_, __) { + return const SizedBox( + width: 16, + ); + }, + itemCount: primaryColors.length, + ); + }, + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final themeCard = _getThemeCard(context); + final previewCard = Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CommonCard( + info: Info( + label: appLocalizations.preview, + iconData: Icons.looks, + ), + child: Container( + height: 200, + ), + ), + ); + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + previewCard, + themeCard, + ], + ), + ); + } +} diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart new file mode 100644 index 0000000..a2f36b1 --- /dev/null +++ b/lib/fragments/tools.dart @@ -0,0 +1,253 @@ +import 'dart:io'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/fragments/about.dart'; +import 'package:fl_clash/fragments/access.dart'; +import 'package:fl_clash/fragments/application_setting.dart'; +import 'package:fl_clash/fragments/config.dart'; +import 'package:fl_clash/l10n/l10n.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import '../widgets/widgets.dart'; +import 'theme.dart'; + +class ToolsFragment extends StatefulWidget { + const ToolsFragment({super.key}); + + @override + State createState() => _ToolboxFragmentState(); +} + +class _ToolboxFragmentState extends State { + _buildNavigationMenuItem(NavigationItem navigationItem) { + return ListItem.open( + leading: navigationItem.icon, + title: Text(Intl.message(navigationItem.label)), + subtitle: navigationItem.description != null + ? Text(Intl.message(navigationItem.description!)) + : null, + delegate: OpenDelegate( + title: Intl.message(navigationItem.label), + widget: navigationItem.fragment, + ), + ); + } + + Widget _buildNavigationMenu(List navigationItems) { + return Column( + children: [ + for (final navigationItem in navigationItems) ...[ + _buildNavigationMenuItem(navigationItem), + navigationItems.last != navigationItem + ? const Divider( + height: 0, + ) + : Container(), + ] + ], + ); + } + + Widget _buildSection({ + required String title, + required Widget content, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text( + title, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + Expanded( + flex: 0, + child: content, + ) + ], + ); + } + + String _getLocaleString(Locale? locale) { + if (locale == null) return appLocalizations.defaultText; + return Intl.message(locale.toString()); + } + + Widget _getOtherList() { + List items = [ + ListItem.open( + leading: const Icon(Icons.info), + title: Text(appLocalizations.about), + delegate: OpenDelegate( + title: appLocalizations.about, + widget: const AboutFragment(), + ), + ), + ]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final item in items) ...[ + item, + if (item != items.last) + const Divider( + height: 0, + ), + ] + ], + ); + } + + Widget _getSettingList() { + List items = [ + Selector( + selector: (_, config) => config.locale, + builder: (_, localeString, __) { + final subTitle = localeString ?? appLocalizations.defaultText; + final currentLocale = Other.getLocaleForString(localeString); + return ListTile( + leading: const Icon(Icons.language_outlined), + title: Text(appLocalizations.language), + subtitle: Text(Intl.message(subTitle)), + onTap: () { + globalState.showCommonDialog( + child: AlertDialog( + title: Text(appLocalizations.language), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + for (final locale in [ + null, + ...AppLocalizations.delegate.supportedLocales + ]) + ListItem.radio( + delegate: RadioDelegate( + value: locale, + groupValue: currentLocale, + onChanged: (Locale? value) { + final config = context.read(); + config.locale = value?.toString(); + Navigator.of(context).pop(); + }, + ), + title: Text(_getLocaleString(locale)), + ) + ], + ), + ), + ), + ); + }, + ); + }, + ), + ListItem.open( + leading: const Icon(Icons.style), + title: Text(appLocalizations.theme), + subtitle: Text(appLocalizations.themeDesc), + delegate: OpenDelegate( + title: appLocalizations.theme, + widget: const ThemeFragment(), + extendPageWidth: 360), + ), + if (Platform.isAndroid) + ListItem.open( + leading: const Icon(Icons.view_list), + title: Text(appLocalizations.accessControl), + subtitle: Text(appLocalizations.accessControlDesc), + delegate: OpenDelegate( + title: appLocalizations.appAccessControl, + widget: const AccessFragment(), + ), + ), + ListItem.open( + leading: const Icon(Icons.edit), + title: Text(appLocalizations.override), + subtitle: Text(appLocalizations.overrideDesc), + delegate: OpenDelegate( + title: appLocalizations.override, + widget: const ConfigFragment(), + ), + ), + ListItem.open( + leading: const Icon(Icons.settings_applications), + title: Text(appLocalizations.application), + subtitle: Text(appLocalizations.applicationDesc), + delegate: OpenDelegate( + title: appLocalizations.application, + widget: const ApplicationSettingFragment(), + ), + ), + ]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final item in items) ...[ + item, + if (item != items.last) + const Divider( + height: 0, + ), + ] + ], + ); + } + + @override + Widget build(BuildContext context) { + final items = [ + LayoutBuilder(builder: (context, container) { + final isMobile = context.isMobile; + if (!isMobile) { + return Container( + margin: const EdgeInsets.only(top: 18), + ); + } + return Selector>( + selector: (_, appState) => appState.navigationItems, + builder: (_, navigationItems, __) { + final moreNavigationItems = navigationItems + .where( + (element) => element.modes.contains(NavigationItemMode.more), + ) + .toList(); + if (moreNavigationItems.isEmpty) { + return Container(); + } + return _buildSection( + title: appLocalizations.more, + content: _buildNavigationMenu(moreNavigationItems), + ); + }, + ); + }), + _buildSection( + title: appLocalizations.settings, + content: _getSettingList(), + ), + _buildSection( + title: appLocalizations.other, + content: _getOtherList(), + ), + ]; + return ListView.builder( + itemCount: items.length, + itemBuilder: (_, index) => items[index], + ); + } +} diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb new file mode 100644 index 0000000..90311cb --- /dev/null +++ b/lib/l10n/arb/intl_en.arb @@ -0,0 +1,123 @@ +{ + "rule": "Rule", + "global": "Global", + "direct": "Direct", + "dashboard": "Dashboard", + "proxies": "Proxies", + "profile": "Profile", + "profiles": "Profiles", + "tools": "Tools", + "logs": "Logs", + "logsDesc": "Log capture records", + "trafficUsage": "Traffic usage", + "coreInfo": "Core info", + "nullCoreInfoDesc": "Unable to obtain core info", + "networkSpeed": "Network speed", + "outboundMode": "Outbound mode", + "networkDetection": "Network detection", + "upload": "Upload", + "download": "Download", + "noProxy": "No proxy", + "noProxyDesc": "Please create a profile or add a valid profile", + "nullProfileDesc": "No profile, Please add a profile", + "nullLogsDesc": "No logs", + "settings": "Settings", + "language": "Language", + "defaultText": "Default", + "more": "More", + "other": "Other", + "about": "About", + "en": "English", + "zh_CN": "Simplified Chinese", + "theme": "Theme", + "themeDesc": "Set dark mode,adjust the color", + "override": "Override", + "overrideDesc": "Override Proxy related config", + "allowLan": "AllowLan", + "allowLanDesc": "Allow access proxy through the LAN", + "tun": "Tun", + "tunDesc": "only effective in administrator mode", + "minimizeOnExit": "Minimize on exit", + "minimizeOnExitDesc": "Modify the default system exit event", + "autoLaunch": "AutoLaunch", + "autoLaunchDesc": "Follow the system self startup", + "silentLaunch": "SilentLaunch", + "silentLaunchDesc": "Start in the background", + "autoRun": "AutoRun", + "autoRunDesc": "Auto run when the application is opened", + "logcat": "Logcat", + "logcatDesc": "Disabling will hide the log entry", + "accessControl": "AccessControl", + "accessControlDesc": "Configure application access proxy", + "application": "Application", + "applicationDesc": "Modify application related settings", + "edit": "Edit", + "confirm": "Confirm", + "update": "Update", + "add": "Add", + "save": "Save", + "delete": "Delete", + "years": "Years", + "months": "Months", + "hours": "Hours", + "days": "Days", + "minutes": "Minutes", + "ago": " Ago", + "just": "Just", + "qrcode": "QR code", + "qrcodeDesc": "Scan QR code to obtain profile", + "url": "URL", + "urlDesc": "Obtain profile through URL", + "file": "File", + "fileDesc": "Directly upload profile", + "name": "Name", + "profileNameNullValidationDesc": "Please input the profile name", + "profileUrlNullValidationDesc": "Please input the profile URL", + "profileUrlInvalidValidationDesc": "Please input a valid profile URL", + "autoUpdate": "Auto update", + "autoUpdateInterval": "Auto update interval (minutes)", + "profileAutoUpdateIntervalNullValidationDesc": "Please enter the auto update interval time", + "profileAutoUpdateIntervalInvalidValidationDesc": "Please input a valid interval time format", + "themeMode": "Theme mode", + "themeColor": "Theme color", + "preview": "Preview", + "auto": "Auto", + "light": "Light", + "dark": "Dark", + "importFromURL": "Import from URL", + "submit": "Submit", + "doYouWantToPass": "Do you want to pass", + "create": "Create", + "defaultSort": "Sort by default", + "delaySort": "Sort by delay", + "nameSort": "Sort by name", + "pleaseUploadFile": "Please upload file", + "blacklistMode": "Blacklist mode", + "whitelistMode": "Whitelist mode", + "filterSystemApp": "Filter system app", + "cancelFilterSystemApp": "Cancel filter system app", + "selectAll": "Select all", + "cancelSelectAll": "Cancel select all", + "appAccessControl": "App access control", + "accessControlAllowDesc": "Only allow selected app to enter VPN", + "accessControlNotAllowDesc": "The selected application will be excluded from VPN", + "selected": "Selected", + "unableToUpdateCurrentProfileDesc": "unable to update current profile", + "noMoreInfoDesc": "No more info", + "profileParseErrorDesc": "profile parse error", + "proxyPort": "ProxyPort", + "port": "Port", + "logLevel": "LogLevel", + "show": "Show", + "exit": "Exit", + "systemProxy": "SystemProxy", + "project": "Project", + "core": "Core", + "checkUpdate": "Check update", + "tabAnimation": "Tab animation", + "tabAnimationDesc": "When enabled, the home tab will add a toggle animation", + "desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", + "startVpn": "Staring VPN...", + "stopVpn": "Stopping VPN...", + "discovery": "Discovery a new version" +} \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb new file mode 100644 index 0000000..e271eef --- /dev/null +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -0,0 +1,123 @@ +{ + "rule": "规则", + "global": "全局", + "direct": "直连", + "dashboard": "仪表盘", + "proxies": "代理", + "profile": "配置", + "profiles": "配置", + "tools": "工具", + "logs": "日志", + "logsDesc": "日志捕获记录", + "trafficUsage": "流量统计", + "coreInfo": "内核信息", + "nullCoreInfoDesc": "无法获取内核信息", + "networkSpeed": "网络速度", + "outboundMode": "出站模式", + "networkDetection": "网络检测", + "upload": "上传", + "download": "下载", + "noProxy": "暂无代理", + "noProxyDesc": "请创建配置文件或者添加有效配置文件", + "nullProfileDesc": "没有配置文件,请先添加配置文件", + "nullLogsDesc": "暂无日志", + "settings": "设置", + "language": "语言", + "defaultText": "默认", + "more": "更多", + "other": "其他", + "about": "关于", + "en": "英语", + "zh_CN": "中文简体", + "theme": "主题", + "themeDesc": "设置深色模式,调整色彩", + "override": "覆写", + "overrideDesc": "覆写代理相关配置", + "allowLan": "局域网代理", + "allowLanDesc": "允许通过局域网访问代理", + "tun": "虚拟网络设备", + "tunDesc": "仅在管理员模式生效", + "minimizeOnExit": "退出时最小化", + "minimizeOnExitDesc": "修改系统默认退出事件", + "autoLaunch": "自启动", + "autoLaunchDesc": "跟随系统自启动", + "silentLaunch": "静默启动", + "silentLaunchDesc": "后台启动", + "autoRun": "自动运行", + "autoRunDesc": "应用打开时自动运行", + "logcat": "日志捕获", + "logcatDesc": "禁用将会隐藏日志入口", + "accessControl": "访问控制", + "accessControlDesc": "配置应用访问代理", + "application": "应用程序", + "applicationDesc": "修改应用程序相关设置", + "edit": "编辑", + "confirm": "确定", + "update": "更新", + "add": "添加", + "save": "保存", + "delete": "删除", + "years": "年", + "months": "月", + "hours": "小时", + "days": "天", + "minutes": "分钟", + "ago": "前", + "just": "刚刚", + "qrcode": "二维码", + "qrcodeDesc": "扫描二维码获取配置文件", + "url": "URL", + "urlDesc": "直接上传配置文件", + "file": "文件", + "fileDesc": "直接上传配置文件", + "name": "名称", + "profileNameNullValidationDesc": "请输入配置名称", + "profileUrlNullValidationDesc": "请输入配置URL", + "profileUrlInvalidValidationDesc": "请输入有效配置URL", + "autoUpdate": "自动更新", + "autoUpdateInterval": "自动更新间隔(分钟)", + "profileAutoUpdateIntervalNullValidationDesc": "请输入自动更新间隔时间", + "profileAutoUpdateIntervalInvalidValidationDesc": "请输入有效间隔时间格式", + "themeMode": "主题模式", + "themeColor": "主题色彩", + "preview": "预览", + "auto": "自动", + "light": "浅色", + "dark": "深色", + "importFromURL": "从URL导入", + "submit": "提交", + "doYouWantToPass": "是否要通过", + "create": "创建", + "defaultSort": "按默认排序", + "delaySort": "按延迟排序", + "nameSort": "按名称排序", + "pleaseUploadFile": "请上传文件", + "blacklistMode": "黑名单模式", + "whitelistMode": "白名单模式", + "filterSystemApp": "过滤系统应用", + "cancelFilterSystemApp": "取消过滤系统应用", + "selectAll": "全选", + "cancelSelectAll": "取消全选", + "appAccessControl": "应用访问控制", + "accessControlAllowDesc": "只允许选中应用进入VPN", + "accessControlNotAllowDesc": "选中应用将会被排除在VPN之外", + "selected": "已选择", + "unableToUpdateCurrentProfileDesc": "无法更新当前配置文件", + "noMoreInfoDesc": "暂无更多信息", + "profileParseErrorDesc": "配置文件解析错误", + "proxyPort": "代理端口", + "port": "端口", + "logLevel": "日志等级", + "show": "显示", + "exit": "退出", + "systemProxy": "系统代理", + "project": "项目", + "core": "内核", + "checkUpdate": "检查更新", + "tabAnimation": "选项卡动画", + "tabAnimationDesc": "开启后,主页选项卡将添加切换动画", + "desc": "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", + "startVpn": "正在启动VPN...", + "stopVpn": "正在停止VPN...", + "discovery": "发现新版本" +} \ No newline at end of file diff --git a/lib/l10n/intl/messages_all.dart b/lib/l10n/intl/messages_all.dart new file mode 100644 index 0000000..bb98700 --- /dev/null +++ b/lib/l10n/intl/messages_all.dart @@ -0,0 +1,67 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; +import 'messages_zh_CN.dart' as messages_zh_cn; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new SynchronousFuture(null), + 'zh_CN': () => new SynchronousFuture(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + case 'zh_CN': + return messages_zh_cn.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) { + var availableLocale = Intl.verifiedLocale( + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new SynchronousFuture(false); + } + var lib = _deferredLibraries[availableLocale]; + lib == null ? new SynchronousFuture(false) : lib(); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new SynchronousFuture(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart new file mode 100644 index 0000000..b0d003d --- /dev/null +++ b/lib/l10n/intl/messages_en.dart @@ -0,0 +1,189 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("About"), + "accessControl": MessageLookupByLibrary.simpleMessage("AccessControl"), + "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage( + "Only allow selected app to enter VPN"), + "accessControlDesc": MessageLookupByLibrary.simpleMessage( + "Configure application access proxy"), + "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( + "The selected application will be excluded from VPN"), + "add": MessageLookupByLibrary.simpleMessage("Add"), + "ago": MessageLookupByLibrary.simpleMessage(" Ago"), + "allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"), + "allowLanDesc": MessageLookupByLibrary.simpleMessage( + "Allow access proxy through the LAN"), + "appAccessControl": + MessageLookupByLibrary.simpleMessage("App access control"), + "application": MessageLookupByLibrary.simpleMessage("Application"), + "applicationDesc": MessageLookupByLibrary.simpleMessage( + "Modify application related settings"), + "auto": MessageLookupByLibrary.simpleMessage("Auto"), + "autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"), + "autoLaunchDesc": MessageLookupByLibrary.simpleMessage( + "Follow the system self startup"), + "autoRun": MessageLookupByLibrary.simpleMessage("AutoRun"), + "autoRunDesc": MessageLookupByLibrary.simpleMessage( + "Auto run when the application is opened"), + "autoUpdate": MessageLookupByLibrary.simpleMessage("Auto update"), + "autoUpdateInterval": MessageLookupByLibrary.simpleMessage( + "Auto update interval (minutes)"), + "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), + "cancelFilterSystemApp": + MessageLookupByLibrary.simpleMessage("Cancel filter system app"), + "cancelSelectAll": + MessageLookupByLibrary.simpleMessage("Cancel select all"), + "checkUpdate": MessageLookupByLibrary.simpleMessage("Check update"), + "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "core": MessageLookupByLibrary.simpleMessage("Core"), + "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), + "create": MessageLookupByLibrary.simpleMessage("Create"), + "dark": MessageLookupByLibrary.simpleMessage("Dark"), + "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), + "days": MessageLookupByLibrary.simpleMessage("Days"), + "defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"), + "defaultText": MessageLookupByLibrary.simpleMessage("Default"), + "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), + "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "desc": MessageLookupByLibrary.simpleMessage( + "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."), + "direct": MessageLookupByLibrary.simpleMessage("Direct"), + "discovery": + MessageLookupByLibrary.simpleMessage("Discovery a new version"), + "doYouWantToPass": + MessageLookupByLibrary.simpleMessage("Do you want to pass"), + "download": MessageLookupByLibrary.simpleMessage("Download"), + "edit": MessageLookupByLibrary.simpleMessage("Edit"), + "en": MessageLookupByLibrary.simpleMessage("English"), + "exit": MessageLookupByLibrary.simpleMessage("Exit"), + "file": MessageLookupByLibrary.simpleMessage("File"), + "fileDesc": + MessageLookupByLibrary.simpleMessage("Directly upload profile"), + "filterSystemApp": + MessageLookupByLibrary.simpleMessage("Filter system app"), + "global": MessageLookupByLibrary.simpleMessage("Global"), + "hours": MessageLookupByLibrary.simpleMessage("Hours"), + "importFromURL": + MessageLookupByLibrary.simpleMessage("Import from URL"), + "just": MessageLookupByLibrary.simpleMessage("Just"), + "language": MessageLookupByLibrary.simpleMessage("Language"), + "light": MessageLookupByLibrary.simpleMessage("Light"), + "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), + "logcat": MessageLookupByLibrary.simpleMessage("Logcat"), + "logcatDesc": MessageLookupByLibrary.simpleMessage( + "Disabling will hide the log entry"), + "logs": MessageLookupByLibrary.simpleMessage("Logs"), + "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), + "minimizeOnExit": + MessageLookupByLibrary.simpleMessage("Minimize on exit"), + "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( + "Modify the default system exit event"), + "minutes": MessageLookupByLibrary.simpleMessage("Minutes"), + "months": MessageLookupByLibrary.simpleMessage("Months"), + "more": MessageLookupByLibrary.simpleMessage("More"), + "name": MessageLookupByLibrary.simpleMessage("Name"), + "nameSort": MessageLookupByLibrary.simpleMessage("Sort by name"), + "networkDetection": + MessageLookupByLibrary.simpleMessage("Network detection"), + "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), + "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"), + "noProxy": MessageLookupByLibrary.simpleMessage("No proxy"), + "noProxyDesc": MessageLookupByLibrary.simpleMessage( + "Please create a profile or add a valid profile"), + "nullCoreInfoDesc": + MessageLookupByLibrary.simpleMessage("Unable to obtain core info"), + "nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"), + "nullProfileDesc": MessageLookupByLibrary.simpleMessage( + "No profile, Please add a profile"), + "other": MessageLookupByLibrary.simpleMessage("Other"), + "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"), + "override": MessageLookupByLibrary.simpleMessage("Override"), + "overrideDesc": MessageLookupByLibrary.simpleMessage( + "Override Proxy related config"), + "pleaseUploadFile": + MessageLookupByLibrary.simpleMessage("Please upload file"), + "port": MessageLookupByLibrary.simpleMessage("Port"), + "preview": MessageLookupByLibrary.simpleMessage("Preview"), + "profile": MessageLookupByLibrary.simpleMessage("Profile"), + "profileAutoUpdateIntervalInvalidValidationDesc": + MessageLookupByLibrary.simpleMessage( + "Please input a valid interval time format"), + "profileAutoUpdateIntervalNullValidationDesc": + MessageLookupByLibrary.simpleMessage( + "Please enter the auto update interval time"), + "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage( + "Please input the profile name"), + "profileParseErrorDesc": + MessageLookupByLibrary.simpleMessage("profile parse error"), + "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage( + "Please input a valid profile URL"), + "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage( + "Please input the profile URL"), + "profiles": MessageLookupByLibrary.simpleMessage("Profiles"), + "project": MessageLookupByLibrary.simpleMessage("Project"), + "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), + "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), + "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), + "qrcodeDesc": MessageLookupByLibrary.simpleMessage( + "Scan QR code to obtain profile"), + "rule": MessageLookupByLibrary.simpleMessage("Rule"), + "save": MessageLookupByLibrary.simpleMessage("Save"), + "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), + "selected": MessageLookupByLibrary.simpleMessage("Selected"), + "settings": MessageLookupByLibrary.simpleMessage("Settings"), + "show": MessageLookupByLibrary.simpleMessage("Show"), + "silentLaunch": MessageLookupByLibrary.simpleMessage("SilentLaunch"), + "silentLaunchDesc": + MessageLookupByLibrary.simpleMessage("Start in the background"), + "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), + "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), + "submit": MessageLookupByLibrary.simpleMessage("Submit"), + "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), + "tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"), + "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( + "When enabled, the home tab will add a toggle animation"), + "theme": MessageLookupByLibrary.simpleMessage("Theme"), + "themeColor": MessageLookupByLibrary.simpleMessage("Theme color"), + "themeDesc": MessageLookupByLibrary.simpleMessage( + "Set dark mode,adjust the color"), + "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), + "tools": MessageLookupByLibrary.simpleMessage("Tools"), + "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), + "tun": MessageLookupByLibrary.simpleMessage("Tun"), + "tunDesc": MessageLookupByLibrary.simpleMessage( + "only effective in administrator mode"), + "unableToUpdateCurrentProfileDesc": + MessageLookupByLibrary.simpleMessage( + "unable to update current profile"), + "update": MessageLookupByLibrary.simpleMessage("Update"), + "upload": MessageLookupByLibrary.simpleMessage("Upload"), + "url": MessageLookupByLibrary.simpleMessage("URL"), + "urlDesc": + MessageLookupByLibrary.simpleMessage("Obtain profile through URL"), + "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), + "years": MessageLookupByLibrary.simpleMessage("Years"), + "zh_CN": MessageLookupByLibrary.simpleMessage("Simplified Chinese") + }; +} diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart new file mode 100644 index 0000000..551df26 --- /dev/null +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -0,0 +1,163 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a zh_CN locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'zh_CN'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("关于"), + "accessControl": MessageLookupByLibrary.simpleMessage("访问控制"), + "accessControlAllowDesc": + MessageLookupByLibrary.simpleMessage("只允许选中应用进入VPN"), + "accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"), + "accessControlNotAllowDesc": + MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), + "add": MessageLookupByLibrary.simpleMessage("添加"), + "ago": MessageLookupByLibrary.simpleMessage("前"), + "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), + "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), + "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), + "application": MessageLookupByLibrary.simpleMessage("应用程序"), + "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"), + "auto": MessageLookupByLibrary.simpleMessage("自动"), + "autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"), + "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), + "autoRun": MessageLookupByLibrary.simpleMessage("自动运行"), + "autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"), + "autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), + "autoUpdateInterval": + MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"), + "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), + "cancelFilterSystemApp": + MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), + "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), + "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), + "confirm": MessageLookupByLibrary.simpleMessage("确定"), + "core": MessageLookupByLibrary.simpleMessage("内核"), + "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), + "create": MessageLookupByLibrary.simpleMessage("创建"), + "dark": MessageLookupByLibrary.simpleMessage("深色"), + "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), + "days": MessageLookupByLibrary.simpleMessage("天"), + "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), + "defaultText": MessageLookupByLibrary.simpleMessage("默认"), + "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), + "delete": MessageLookupByLibrary.simpleMessage("删除"), + "desc": MessageLookupByLibrary.simpleMessage( + "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"), + "direct": MessageLookupByLibrary.simpleMessage("直连"), + "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), + "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), + "download": MessageLookupByLibrary.simpleMessage("下载"), + "edit": MessageLookupByLibrary.simpleMessage("编辑"), + "en": MessageLookupByLibrary.simpleMessage("英语"), + "exit": MessageLookupByLibrary.simpleMessage("退出"), + "file": MessageLookupByLibrary.simpleMessage("文件"), + "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), + "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), + "global": MessageLookupByLibrary.simpleMessage("全局"), + "hours": MessageLookupByLibrary.simpleMessage("小时"), + "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), + "just": MessageLookupByLibrary.simpleMessage("刚刚"), + "language": MessageLookupByLibrary.simpleMessage("语言"), + "light": MessageLookupByLibrary.simpleMessage("浅色"), + "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), + "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), + "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), + "logs": MessageLookupByLibrary.simpleMessage("日志"), + "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), + "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), + "minimizeOnExitDesc": + MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), + "minutes": MessageLookupByLibrary.simpleMessage("分钟"), + "months": MessageLookupByLibrary.simpleMessage("月"), + "more": MessageLookupByLibrary.simpleMessage("更多"), + "name": MessageLookupByLibrary.simpleMessage("名称"), + "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), + "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), + "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), + "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), + "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), + "noProxyDesc": + MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), + "nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"), + "nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"), + "nullProfileDesc": + MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), + "other": MessageLookupByLibrary.simpleMessage("其他"), + "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), + "override": MessageLookupByLibrary.simpleMessage("覆写"), + "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), + "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), + "port": MessageLookupByLibrary.simpleMessage("端口"), + "preview": MessageLookupByLibrary.simpleMessage("预览"), + "profile": MessageLookupByLibrary.simpleMessage("配置"), + "profileAutoUpdateIntervalInvalidValidationDesc": + MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"), + "profileAutoUpdateIntervalNullValidationDesc": + MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"), + "profileNameNullValidationDesc": + MessageLookupByLibrary.simpleMessage("请输入配置名称"), + "profileParseErrorDesc": + MessageLookupByLibrary.simpleMessage("配置文件解析错误"), + "profileUrlInvalidValidationDesc": + MessageLookupByLibrary.simpleMessage("请输入有效配置URL"), + "profileUrlNullValidationDesc": + MessageLookupByLibrary.simpleMessage("请输入配置URL"), + "profiles": MessageLookupByLibrary.simpleMessage("配置"), + "project": MessageLookupByLibrary.simpleMessage("项目"), + "proxies": MessageLookupByLibrary.simpleMessage("代理"), + "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), + "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), + "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), + "rule": MessageLookupByLibrary.simpleMessage("规则"), + "save": MessageLookupByLibrary.simpleMessage("保存"), + "selectAll": MessageLookupByLibrary.simpleMessage("全选"), + "selected": MessageLookupByLibrary.simpleMessage("已选择"), + "settings": MessageLookupByLibrary.simpleMessage("设置"), + "show": MessageLookupByLibrary.simpleMessage("显示"), + "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), + "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), + "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), + "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), + "submit": MessageLookupByLibrary.simpleMessage("提交"), + "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), + "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), + "tabAnimationDesc": + MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), + "theme": MessageLookupByLibrary.simpleMessage("主题"), + "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), + "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), + "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), + "tools": MessageLookupByLibrary.simpleMessage("工具"), + "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), + "tun": MessageLookupByLibrary.simpleMessage("虚拟网络设备"), + "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), + "unableToUpdateCurrentProfileDesc": + MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), + "update": MessageLookupByLibrary.simpleMessage("更新"), + "upload": MessageLookupByLibrary.simpleMessage("上传"), + "url": MessageLookupByLibrary.simpleMessage("URL"), + "urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), + "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), + "years": MessageLookupByLibrary.simpleMessage("年"), + "zh_CN": MessageLookupByLibrary.simpleMessage("中文简体") + }; +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100644 index 0000000..e90f971 --- /dev/null +++ b/lib/l10n/l10n.dart @@ -0,0 +1,1289 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class AppLocalizations { + AppLocalizations(); + + static AppLocalizations? _current; + + static AppLocalizations get current { + assert(_current != null, + 'No instance of AppLocalizations was loaded. Try to initialize the AppLocalizations delegate before accessing AppLocalizations.current.'); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = AppLocalizations(); + AppLocalizations._current = instance; + + return instance; + }); + } + + static AppLocalizations of(BuildContext context) { + final instance = AppLocalizations.maybeOf(context); + assert(instance != null, + 'No instance of AppLocalizations present in the widget tree. Did you add AppLocalizations.delegate in localizationsDelegates?'); + return instance!; + } + + static AppLocalizations? maybeOf(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + /// `Rule` + String get rule { + return Intl.message( + 'Rule', + name: 'rule', + desc: '', + args: [], + ); + } + + /// `Global` + String get global { + return Intl.message( + 'Global', + name: 'global', + desc: '', + args: [], + ); + } + + /// `Direct` + String get direct { + return Intl.message( + 'Direct', + name: 'direct', + desc: '', + args: [], + ); + } + + /// `Dashboard` + String get dashboard { + return Intl.message( + 'Dashboard', + name: 'dashboard', + desc: '', + args: [], + ); + } + + /// `Proxies` + String get proxies { + return Intl.message( + 'Proxies', + name: 'proxies', + desc: '', + args: [], + ); + } + + /// `Profile` + String get profile { + return Intl.message( + 'Profile', + name: 'profile', + desc: '', + args: [], + ); + } + + /// `Profiles` + String get profiles { + return Intl.message( + 'Profiles', + name: 'profiles', + desc: '', + args: [], + ); + } + + /// `Tools` + String get tools { + return Intl.message( + 'Tools', + name: 'tools', + desc: '', + args: [], + ); + } + + /// `Logs` + String get logs { + return Intl.message( + 'Logs', + name: 'logs', + desc: '', + args: [], + ); + } + + /// `Log capture records` + String get logsDesc { + return Intl.message( + 'Log capture records', + name: 'logsDesc', + desc: '', + args: [], + ); + } + + /// `Traffic usage` + String get trafficUsage { + return Intl.message( + 'Traffic usage', + name: 'trafficUsage', + desc: '', + args: [], + ); + } + + /// `Core info` + String get coreInfo { + return Intl.message( + 'Core info', + name: 'coreInfo', + desc: '', + args: [], + ); + } + + /// `Unable to obtain core info` + String get nullCoreInfoDesc { + return Intl.message( + 'Unable to obtain core info', + name: 'nullCoreInfoDesc', + desc: '', + args: [], + ); + } + + /// `Network speed` + String get networkSpeed { + return Intl.message( + 'Network speed', + name: 'networkSpeed', + desc: '', + args: [], + ); + } + + /// `Outbound mode` + String get outboundMode { + return Intl.message( + 'Outbound mode', + name: 'outboundMode', + desc: '', + args: [], + ); + } + + /// `Network detection` + String get networkDetection { + return Intl.message( + 'Network detection', + name: 'networkDetection', + desc: '', + args: [], + ); + } + + /// `Upload` + String get upload { + return Intl.message( + 'Upload', + name: 'upload', + desc: '', + args: [], + ); + } + + /// `Download` + String get download { + return Intl.message( + 'Download', + name: 'download', + desc: '', + args: [], + ); + } + + /// `No proxy` + String get noProxy { + return Intl.message( + 'No proxy', + name: 'noProxy', + desc: '', + args: [], + ); + } + + /// `Please create a profile or add a valid profile` + String get noProxyDesc { + return Intl.message( + 'Please create a profile or add a valid profile', + name: 'noProxyDesc', + desc: '', + args: [], + ); + } + + /// `No profile, Please add a profile` + String get nullProfileDesc { + return Intl.message( + 'No profile, Please add a profile', + name: 'nullProfileDesc', + desc: '', + args: [], + ); + } + + /// `No logs` + String get nullLogsDesc { + return Intl.message( + 'No logs', + name: 'nullLogsDesc', + desc: '', + args: [], + ); + } + + /// `Settings` + String get settings { + return Intl.message( + 'Settings', + name: 'settings', + desc: '', + args: [], + ); + } + + /// `Language` + String get language { + return Intl.message( + 'Language', + name: 'language', + desc: '', + args: [], + ); + } + + /// `Default` + String get defaultText { + return Intl.message( + 'Default', + name: 'defaultText', + desc: '', + args: [], + ); + } + + /// `More` + String get more { + return Intl.message( + 'More', + name: 'more', + desc: '', + args: [], + ); + } + + /// `Other` + String get other { + return Intl.message( + 'Other', + name: 'other', + desc: '', + args: [], + ); + } + + /// `About` + String get about { + return Intl.message( + 'About', + name: 'about', + desc: '', + args: [], + ); + } + + /// `English` + String get en { + return Intl.message( + 'English', + name: 'en', + desc: '', + args: [], + ); + } + + /// `Simplified Chinese` + String get zh_CN { + return Intl.message( + 'Simplified Chinese', + name: 'zh_CN', + desc: '', + args: [], + ); + } + + /// `Theme` + String get theme { + return Intl.message( + 'Theme', + name: 'theme', + desc: '', + args: [], + ); + } + + /// `Set dark mode,adjust the color` + String get themeDesc { + return Intl.message( + 'Set dark mode,adjust the color', + name: 'themeDesc', + desc: '', + args: [], + ); + } + + /// `Override` + String get override { + return Intl.message( + 'Override', + name: 'override', + desc: '', + args: [], + ); + } + + /// `Override Proxy related config` + String get overrideDesc { + return Intl.message( + 'Override Proxy related config', + name: 'overrideDesc', + desc: '', + args: [], + ); + } + + /// `AllowLan` + String get allowLan { + return Intl.message( + 'AllowLan', + name: 'allowLan', + desc: '', + args: [], + ); + } + + /// `Allow access proxy through the LAN` + String get allowLanDesc { + return Intl.message( + 'Allow access proxy through the LAN', + name: 'allowLanDesc', + desc: '', + args: [], + ); + } + + /// `Tun` + String get tun { + return Intl.message( + 'Tun', + name: 'tun', + desc: '', + args: [], + ); + } + + /// `only effective in administrator mode` + String get tunDesc { + return Intl.message( + 'only effective in administrator mode', + name: 'tunDesc', + desc: '', + args: [], + ); + } + + /// `Minimize on exit` + String get minimizeOnExit { + return Intl.message( + 'Minimize on exit', + name: 'minimizeOnExit', + desc: '', + args: [], + ); + } + + /// `Modify the default system exit event` + String get minimizeOnExitDesc { + return Intl.message( + 'Modify the default system exit event', + name: 'minimizeOnExitDesc', + desc: '', + args: [], + ); + } + + /// `AutoLaunch` + String get autoLaunch { + return Intl.message( + 'AutoLaunch', + name: 'autoLaunch', + desc: '', + args: [], + ); + } + + /// `Follow the system self startup` + String get autoLaunchDesc { + return Intl.message( + 'Follow the system self startup', + name: 'autoLaunchDesc', + desc: '', + args: [], + ); + } + + /// `SilentLaunch` + String get silentLaunch { + return Intl.message( + 'SilentLaunch', + name: 'silentLaunch', + desc: '', + args: [], + ); + } + + /// `Start in the background` + String get silentLaunchDesc { + return Intl.message( + 'Start in the background', + name: 'silentLaunchDesc', + desc: '', + args: [], + ); + } + + /// `AutoRun` + String get autoRun { + return Intl.message( + 'AutoRun', + name: 'autoRun', + desc: '', + args: [], + ); + } + + /// `Auto run when the application is opened` + String get autoRunDesc { + return Intl.message( + 'Auto run when the application is opened', + name: 'autoRunDesc', + desc: '', + args: [], + ); + } + + /// `Logcat` + String get logcat { + return Intl.message( + 'Logcat', + name: 'logcat', + desc: '', + args: [], + ); + } + + /// `Disabling will hide the log entry` + String get logcatDesc { + return Intl.message( + 'Disabling will hide the log entry', + name: 'logcatDesc', + desc: '', + args: [], + ); + } + + /// `AccessControl` + String get accessControl { + return Intl.message( + 'AccessControl', + name: 'accessControl', + desc: '', + args: [], + ); + } + + /// `Configure application access proxy` + String get accessControlDesc { + return Intl.message( + 'Configure application access proxy', + name: 'accessControlDesc', + desc: '', + args: [], + ); + } + + /// `Application` + String get application { + return Intl.message( + 'Application', + name: 'application', + desc: '', + args: [], + ); + } + + /// `Modify application related settings` + String get applicationDesc { + return Intl.message( + 'Modify application related settings', + name: 'applicationDesc', + desc: '', + args: [], + ); + } + + /// `Edit` + String get edit { + return Intl.message( + 'Edit', + name: 'edit', + desc: '', + args: [], + ); + } + + /// `Confirm` + String get confirm { + return Intl.message( + 'Confirm', + name: 'confirm', + desc: '', + args: [], + ); + } + + /// `Update` + String get update { + return Intl.message( + 'Update', + name: 'update', + desc: '', + args: [], + ); + } + + /// `Add` + String get add { + return Intl.message( + 'Add', + name: 'add', + desc: '', + args: [], + ); + } + + /// `Save` + String get save { + return Intl.message( + 'Save', + name: 'save', + desc: '', + args: [], + ); + } + + /// `Delete` + String get delete { + return Intl.message( + 'Delete', + name: 'delete', + desc: '', + args: [], + ); + } + + /// `Years` + String get years { + return Intl.message( + 'Years', + name: 'years', + desc: '', + args: [], + ); + } + + /// `Months` + String get months { + return Intl.message( + 'Months', + name: 'months', + desc: '', + args: [], + ); + } + + /// `Hours` + String get hours { + return Intl.message( + 'Hours', + name: 'hours', + desc: '', + args: [], + ); + } + + /// `Days` + String get days { + return Intl.message( + 'Days', + name: 'days', + desc: '', + args: [], + ); + } + + /// `Minutes` + String get minutes { + return Intl.message( + 'Minutes', + name: 'minutes', + desc: '', + args: [], + ); + } + + /// ` Ago` + String get ago { + return Intl.message( + ' Ago', + name: 'ago', + desc: '', + args: [], + ); + } + + /// `Just` + String get just { + return Intl.message( + 'Just', + name: 'just', + desc: '', + args: [], + ); + } + + /// `QR code` + String get qrcode { + return Intl.message( + 'QR code', + name: 'qrcode', + desc: '', + args: [], + ); + } + + /// `Scan QR code to obtain profile` + String get qrcodeDesc { + return Intl.message( + 'Scan QR code to obtain profile', + name: 'qrcodeDesc', + desc: '', + args: [], + ); + } + + /// `URL` + String get url { + return Intl.message( + 'URL', + name: 'url', + desc: '', + args: [], + ); + } + + /// `Obtain profile through URL` + String get urlDesc { + return Intl.message( + 'Obtain profile through URL', + name: 'urlDesc', + desc: '', + args: [], + ); + } + + /// `File` + String get file { + return Intl.message( + 'File', + name: 'file', + desc: '', + args: [], + ); + } + + /// `Directly upload profile` + String get fileDesc { + return Intl.message( + 'Directly upload profile', + name: 'fileDesc', + desc: '', + args: [], + ); + } + + /// `Name` + String get name { + return Intl.message( + 'Name', + name: 'name', + desc: '', + args: [], + ); + } + + /// `Please input the profile name` + String get profileNameNullValidationDesc { + return Intl.message( + 'Please input the profile name', + name: 'profileNameNullValidationDesc', + desc: '', + args: [], + ); + } + + /// `Please input the profile URL` + String get profileUrlNullValidationDesc { + return Intl.message( + 'Please input the profile URL', + name: 'profileUrlNullValidationDesc', + desc: '', + args: [], + ); + } + + /// `Please input a valid profile URL` + String get profileUrlInvalidValidationDesc { + return Intl.message( + 'Please input a valid profile URL', + name: 'profileUrlInvalidValidationDesc', + desc: '', + args: [], + ); + } + + /// `Auto update` + String get autoUpdate { + return Intl.message( + 'Auto update', + name: 'autoUpdate', + desc: '', + args: [], + ); + } + + /// `Auto update interval (minutes)` + String get autoUpdateInterval { + return Intl.message( + 'Auto update interval (minutes)', + name: 'autoUpdateInterval', + desc: '', + args: [], + ); + } + + /// `Please enter the auto update interval time` + String get profileAutoUpdateIntervalNullValidationDesc { + return Intl.message( + 'Please enter the auto update interval time', + name: 'profileAutoUpdateIntervalNullValidationDesc', + desc: '', + args: [], + ); + } + + /// `Please input a valid interval time format` + String get profileAutoUpdateIntervalInvalidValidationDesc { + return Intl.message( + 'Please input a valid interval time format', + name: 'profileAutoUpdateIntervalInvalidValidationDesc', + desc: '', + args: [], + ); + } + + /// `Theme mode` + String get themeMode { + return Intl.message( + 'Theme mode', + name: 'themeMode', + desc: '', + args: [], + ); + } + + /// `Theme color` + String get themeColor { + return Intl.message( + 'Theme color', + name: 'themeColor', + desc: '', + args: [], + ); + } + + /// `Preview` + String get preview { + return Intl.message( + 'Preview', + name: 'preview', + desc: '', + args: [], + ); + } + + /// `Auto` + String get auto { + return Intl.message( + 'Auto', + name: 'auto', + desc: '', + args: [], + ); + } + + /// `Light` + String get light { + return Intl.message( + 'Light', + name: 'light', + desc: '', + args: [], + ); + } + + /// `Dark` + String get dark { + return Intl.message( + 'Dark', + name: 'dark', + desc: '', + args: [], + ); + } + + /// `Import from URL` + String get importFromURL { + return Intl.message( + 'Import from URL', + name: 'importFromURL', + desc: '', + args: [], + ); + } + + /// `Submit` + String get submit { + return Intl.message( + 'Submit', + name: 'submit', + desc: '', + args: [], + ); + } + + /// `Do you want to pass` + String get doYouWantToPass { + return Intl.message( + 'Do you want to pass', + name: 'doYouWantToPass', + desc: '', + args: [], + ); + } + + /// `Create` + String get create { + return Intl.message( + 'Create', + name: 'create', + desc: '', + args: [], + ); + } + + /// `Sort by default` + String get defaultSort { + return Intl.message( + 'Sort by default', + name: 'defaultSort', + desc: '', + args: [], + ); + } + + /// `Sort by delay` + String get delaySort { + return Intl.message( + 'Sort by delay', + name: 'delaySort', + desc: '', + args: [], + ); + } + + /// `Sort by name` + String get nameSort { + return Intl.message( + 'Sort by name', + name: 'nameSort', + desc: '', + args: [], + ); + } + + /// `Please upload file` + String get pleaseUploadFile { + return Intl.message( + 'Please upload file', + name: 'pleaseUploadFile', + desc: '', + args: [], + ); + } + + /// `Blacklist mode` + String get blacklistMode { + return Intl.message( + 'Blacklist mode', + name: 'blacklistMode', + desc: '', + args: [], + ); + } + + /// `Whitelist mode` + String get whitelistMode { + return Intl.message( + 'Whitelist mode', + name: 'whitelistMode', + desc: '', + args: [], + ); + } + + /// `Filter system app` + String get filterSystemApp { + return Intl.message( + 'Filter system app', + name: 'filterSystemApp', + desc: '', + args: [], + ); + } + + /// `Cancel filter system app` + String get cancelFilterSystemApp { + return Intl.message( + 'Cancel filter system app', + name: 'cancelFilterSystemApp', + desc: '', + args: [], + ); + } + + /// `Select all` + String get selectAll { + return Intl.message( + 'Select all', + name: 'selectAll', + desc: '', + args: [], + ); + } + + /// `Cancel select all` + String get cancelSelectAll { + return Intl.message( + 'Cancel select all', + name: 'cancelSelectAll', + desc: '', + args: [], + ); + } + + /// `App access control` + String get appAccessControl { + return Intl.message( + 'App access control', + name: 'appAccessControl', + desc: '', + args: [], + ); + } + + /// `Only allow selected app to enter VPN` + String get accessControlAllowDesc { + return Intl.message( + 'Only allow selected app to enter VPN', + name: 'accessControlAllowDesc', + desc: '', + args: [], + ); + } + + /// `The selected application will be excluded from VPN` + String get accessControlNotAllowDesc { + return Intl.message( + 'The selected application will be excluded from VPN', + name: 'accessControlNotAllowDesc', + desc: '', + args: [], + ); + } + + /// `Selected` + String get selected { + return Intl.message( + 'Selected', + name: 'selected', + desc: '', + args: [], + ); + } + + /// `unable to update current profile` + String get unableToUpdateCurrentProfileDesc { + return Intl.message( + 'unable to update current profile', + name: 'unableToUpdateCurrentProfileDesc', + desc: '', + args: [], + ); + } + + /// `No more info` + String get noMoreInfoDesc { + return Intl.message( + 'No more info', + name: 'noMoreInfoDesc', + desc: '', + args: [], + ); + } + + /// `profile parse error` + String get profileParseErrorDesc { + return Intl.message( + 'profile parse error', + name: 'profileParseErrorDesc', + desc: '', + args: [], + ); + } + + /// `ProxyPort` + String get proxyPort { + return Intl.message( + 'ProxyPort', + name: 'proxyPort', + desc: '', + args: [], + ); + } + + /// `Port` + String get port { + return Intl.message( + 'Port', + name: 'port', + desc: '', + args: [], + ); + } + + /// `LogLevel` + String get logLevel { + return Intl.message( + 'LogLevel', + name: 'logLevel', + desc: '', + args: [], + ); + } + + /// `Show` + String get show { + return Intl.message( + 'Show', + name: 'show', + desc: '', + args: [], + ); + } + + /// `Exit` + String get exit { + return Intl.message( + 'Exit', + name: 'exit', + desc: '', + args: [], + ); + } + + /// `SystemProxy` + String get systemProxy { + return Intl.message( + 'SystemProxy', + name: 'systemProxy', + desc: '', + args: [], + ); + } + + /// `Project` + String get project { + return Intl.message( + 'Project', + name: 'project', + desc: '', + args: [], + ); + } + + /// `Core` + String get core { + return Intl.message( + 'Core', + name: 'core', + desc: '', + args: [], + ); + } + + /// `Check update` + String get checkUpdate { + return Intl.message( + 'Check update', + name: 'checkUpdate', + desc: '', + args: [], + ); + } + + /// `Tab animation` + String get tabAnimation { + return Intl.message( + 'Tab animation', + name: 'tabAnimation', + desc: '', + args: [], + ); + } + + /// `When enabled, the home tab will add a toggle animation` + String get tabAnimationDesc { + return Intl.message( + 'When enabled, the home tab will add a toggle animation', + name: 'tabAnimationDesc', + desc: '', + args: [], + ); + } + + /// `A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.` + String get desc { + return Intl.message( + 'A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.', + name: 'desc', + desc: '', + args: [], + ); + } + + /// `Staring VPN...` + String get startVpn { + return Intl.message( + 'Staring VPN...', + name: 'startVpn', + desc: '', + args: [], + ); + } + + /// `Stopping VPN...` + String get stopVpn { + return Intl.message( + 'Stopping VPN...', + name: 'stopVpn', + desc: '', + args: [], + ); + } + + /// `Discovery a new version` + String get discovery { + return Intl.message( + 'Discovery a new version', + name: 'discovery', + desc: '', + args: [], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => AppLocalizations.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..10b1c93 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/plugins/app.dart'; +import 'package:fl_clash/plugins/tile.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'application.dart'; +import 'l10n/l10n.dart'; +import 'models/models.dart'; +import 'common/common.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await android?.init(); + await window?.init(); + final config = await preferences.getConfig() ?? Config(); + final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); + final appState = AppState(); + await globalState.init( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + globalState.updateNavigationItems( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + runAppWithPreferences( + const Application(), + appState: appState, + config: config, + clashConfig: clashConfig, + ); +} + +@pragma('vm:entry-point') +Future vpnService() async { + WidgetsFlutterBinding.ensureInitialized(); + final config = await preferences.getConfig() ?? Config(); + final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); + final appState = AppState(); + clashMessage.addListener(ClashMessageListenerWithVpn(onTun: (String fd) { + proxyManager.setProtect( + int.parse(fd), + ); + })); + await globalState.init( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + + final appLocalizations = await AppLocalizations.load( + Other.getLocaleForString(config.locale) ?? + WidgetsBinding.instance.platformDispatcher.locale, + ); + + handleStart() async { + await app?.tip(appLocalizations.startVpn); + await globalState.startSystemProxy( + config: config, + clashConfig: clashConfig, + ); + globalState.updateTraffic(config: config); + globalState.updateFunctionLists = [ + () { + globalState.updateTraffic(config: config); + } + ]; + } + + if (appState.isInit) { + handleStart(); + tile?.addListener( + TileListenerWithVpn( + onStop: () async { + await app?.tip(appLocalizations.stopVpn); + clashCore.shutdown(); + exit(0); + }, + ), + ); + } +} + +class ClashMessageListenerWithVpn with ClashMessageListener { + final Function(String fd) _onTun; + + ClashMessageListenerWithVpn({ + required Function(String fd) onTun, + }) : _onTun = onTun; + + @override + void onTun(String fd) { + _onTun(fd); + } +} + +class TileListenerWithVpn with TileListener { + final Function() _onStop; + + TileListenerWithVpn({ + required Function() onStop, + }) : _onStop = onStop; + + @override + void onStop() { + _onStop(); + } +} diff --git a/lib/models/app.dart b/lib/models/app.dart new file mode 100644 index 0000000..cbeadec --- /dev/null +++ b/lib/models/app.dart @@ -0,0 +1,191 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/material.dart'; +import 'ffi.dart'; +import 'log.dart'; +import 'navigation.dart'; +import 'package.dart'; +import 'proxy.dart'; +import 'system_color_scheme.dart'; +import 'traffic.dart'; +import 'version.dart'; + +class AppState with ChangeNotifier { + List _navigationItems; + int? _runTime; + bool _isInit; + DelayMap _delayMap; + VersionInfo? _versionInfo; + List _traffics; + List _logs; + List _packages; + String _currentLabel; + SystemColorSchemes _systemColorSchemes; + List _groups; + + AppState() + : _navigationItems = [], + _delayMap = {}, + _isInit = false, + _currentLabel = "dashboard", + _traffics = [], + _logs = [], + _groups = [], + _packages = [], + _systemColorSchemes = SystemColorSchemes(); + + String get currentLabel => _currentLabel; + + set currentLabel(String value) { + if (_currentLabel != value) { + _currentLabel = value; + notifyListeners(); + } + } + + List get navigationItems => _navigationItems; + + set navigationItems(List value) { + if (!const ListEquality().equals(_navigationItems, value)) { + _navigationItems = value; + notifyListeners(); + } + } + + bool get isInit => _isInit; + + set isInit(bool value) { + if (_isInit != value) { + _isInit = value; + notifyListeners(); + } + } + + int? get runTime => _runTime; + + set runTime(int? value) { + if (_runTime != value) { + _runTime = value; + notifyListeners(); + } + } + + DelayMap get delayMap => _delayMap; + + set delayMap(DelayMap value) { + if (_delayMap != value) { + _delayMap = value; + notifyListeners(); + } + } + + setDelay(Delay delay) { + if (_delayMap[delay.name] != delay.value) { + _delayMap = Map.from(_delayMap)..[delay.name] = delay.value; + notifyListeners(); + } + } + + VersionInfo? get versionInfo => _versionInfo; + + set versionInfo(VersionInfo? value) { + if (_versionInfo != value) { + _versionInfo = value; + notifyListeners(); + } + } + + List get traffics => _traffics; + + set traffics(List value) { + if (_traffics != value) { + _traffics = value; + notifyListeners(); + } + } + + addTraffic(Traffic value) { + _traffics = List.from(_traffics)..add(value); + notifyListeners(); + } + + List get logs => _logs; + + set logs(List value) { + if (_logs != value) { + _logs = value; + notifyListeners(); + } + } + + addLog(Log log) { + _logs.add(log); + notifyListeners(); + } + + SystemColorSchemes get systemColorSchemes => _systemColorSchemes; + + set systemColorSchemes(SystemColorSchemes value) { + if (_systemColorSchemes != value) { + _systemColorSchemes = value; + notifyListeners(); + } + } + + List get packages => _packages; + + set packages(List value) { + if (_packages != value) { + _packages = value; + notifyListeners(); + } + } + + List get groups => _groups; + + set groups(List value) { + if (_groups != value) { + _groups = value; + notifyListeners(); + } + } + + List getCurrentGroups(Mode mode) { + switch (mode) { + case Mode.direct: + return []; + case Mode.global: + return groups + .where((element) => element.name == UsedProxy.GLOBAL.name) + .toList(); + case Mode.rule: + return groups + .where((element) => element.name != UsedProxy.GLOBAL.name) + .toList(); + } + } + + String? getCurrentGroupName(String? groupName, Mode mode) { + final currentGroups = getCurrentGroups(mode); + switch (mode) { + case Mode.direct: + return null; + case Mode.global: + return UsedProxy.GLOBAL.name; + case Mode.rule: + return groupName ?? + (currentGroups.isNotEmpty ? currentGroups.first.name : null); + } + } + + String? getCurrentProxyName(String? proxyName, Mode mode) { + final currentGroups = getCurrentGroups(mode); + switch (mode) { + case Mode.direct: + return UsedProxy.DIRECT.name; + case Mode.global || Mode.rule: + return proxyName ?? + (currentGroups.isNotEmpty ? currentGroups.first.now : null); + } + } +} diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart new file mode 100644 index 0000000..849e6ba --- /dev/null +++ b/lib/models/clash_config.dart @@ -0,0 +1,228 @@ +import 'package:fl_clash/common/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../enum/enum.dart'; + +part 'generated/clash_config.g.dart'; + +@JsonSerializable() +class Tun { + bool enable; + String device; + TunStack stack; + @JsonKey(name: "dns-hijack") + List dnsHijack; + + Tun() : enable = false, + stack = TunStack.gvisor, + dnsHijack = ["any:53"], + device = appConstant.name; + + factory Tun.fromJson(Map json) { + return _$TunFromJson(json); + } + + Map toJson() { + return _$TunToJson(this); + } + + // Tun copyWith({bool? enable, int? fileDescriptor}) { + // return Tun( + // enable: enable ?? this.enable, + // ); + // } +} + +@JsonSerializable() +class Dns { + bool enable; + bool ipv6; + @JsonKey(name: "default-nameserver") + List defaultNameserver; + @JsonKey(name: "enhanced-mode") + String enhancedMode; + @JsonKey(name: "fake-ip-range") + String fakeIpRange; + @JsonKey(name: "use-hosts") + bool useHosts; + List nameserver; + List fallback; + @JsonKey(name: "fake-ip-filter") + List fakeIpFilter; + + Dns() + : enable = true, + ipv6 = false, + defaultNameserver = [ + "223.5.5.5", + "119.29.29.29", + "8.8.4.4", + "1.0.0.1", + ], + enhancedMode = "fake-ip", + fakeIpRange = "198.18.0.1/16", + useHosts = true, + nameserver = [ + "8.8.8.8", + "114.114.114.114", + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query", + ], + fallback = [ + 'https://doh.dns.sb/dns-query', + 'https://dns.cloudflare.com/dns-query', + 'https://dns.twnic.tw/dns-query', + 'tls://8.8.4.4:853', + ], + fakeIpFilter = [ + // Stun Services + "+.stun.*.*", + "+.stun.*.*.*", + "+.stun.*.*.*.*", + "+.stun.*.*.*.*.*", + + // Google Voices + "lens.l.google.com", + + // Nintendo Switch STUN + "*.n.n.srv.nintendo.net", + + // PlayStation STUN + "+.stun.playstation.net", + + // XBox + "xbox.*.*.microsoft.com", + "*.*.xboxlive.com", + + // Microsoft Captive Portal + "*.msftncsi.com", + "*.msftconnecttest.com", + + // Bilibili CDN + "*.mcdn.bilivideo.cn", + + // Windows Default LAN WorkGroup + "WORKGROUP", + ]; + + factory Dns.fromJson(Map json) { + return _$DnsFromJson(json); + } + + Map toJson() { + return _$DnsToJson(this); + } +} + +@JsonSerializable() +class ClashConfig extends ChangeNotifier { + int _mixedPort; + bool _allowLan; + Mode _mode; + LogLevel _logLevel; + Tun _tun; + Dns _dns; + List _rules; + + ClashConfig({ + int? mixedPort, + Mode? mode, + bool? allowLan, + LogLevel? logLevel, + Tun? tun, + Dns? dns, + List? rules, + }) : _mixedPort = mixedPort ?? 7890, + _mode = mode ?? Mode.rule, + _allowLan = allowLan ?? false, + _logLevel = logLevel ?? LogLevel.info, + _tun = tun ?? Tun(), + _dns = dns ?? Dns(), + _rules = rules ?? []; + + @JsonKey(name: "mixed-port") + int get mixedPort => _mixedPort; + + set mixedPort(int value) { + if (_mixedPort != value) { + _mixedPort = value; + notifyListeners(); + } + } + + Mode get mode => _mode; + + set mode(Mode value) { + if (_mode != value) { + _mode = value; + notifyListeners(); + } + } + + @JsonKey(name: "allow-lan") + bool get allowLan => _allowLan; + + set allowLan(bool value) { + if (_allowLan != value) { + _allowLan = value; + notifyListeners(); + } + } + + @JsonKey(name: "log-level") + LogLevel get logLevel => _logLevel; + + set logLevel(LogLevel value) { + if (_logLevel != value) { + _logLevel = value; + notifyListeners(); + } + } + + Tun get tun => _tun; + + set tun(Tun value) { + if (_tun != value) { + _tun = value; + notifyListeners(); + } + } + + Dns get dns => _dns; + + set dns(Dns value) { + if (_dns != value) { + _dns = value; + notifyListeners(); + } + } + + List get rules => _rules; + + set rules(List value) { + if (_rules != value) { + _rules = value; + notifyListeners(); + } + } + + Map toJson() { + return _$ClashConfigToJson(this); + } + + factory ClashConfig.fromJson(Map json) { + return _$ClashConfigFromJson(json); + } + + ClashConfig copyWith({Tun? tun}) { + return ClashConfig( + mixedPort: mixedPort, + mode: mode, + logLevel: logLevel, + tun: tun ?? this.tun, + dns: dns, + allowLan: allowLan, + ); + } +} diff --git a/lib/models/common.dart b/lib/models/common.dart new file mode 100644 index 0000000..b72042e --- /dev/null +++ b/lib/models/common.dart @@ -0,0 +1,28 @@ +import 'package:fl_clash/enum/enum.dart'; + +class Result { + String? message; + ResultType type; + T? data; + + Result({ + this.message, + required this.type, + this.data, + }); + + Result.success({ + this.data, + }) : type = ResultType.success, + message = null; + + Result.error({ + this.message, + }) : type = ResultType.error, + data = null; + + @override + String toString() { + return 'Result{message: $message, type: $type, data: $data}'; + } +} diff --git a/lib/models/config.dart b/lib/models/config.dart new file mode 100644 index 0000000..bc8e32f --- /dev/null +++ b/lib/models/config.dart @@ -0,0 +1,303 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../enum/enum.dart'; +import '../common/common.dart'; +import 'models.dart'; + +part 'generated/config.g.dart'; + +@JsonSerializable() +class AccessControl { + AccessControlMode mode; + List acceptList; + List rejectList; + bool isFilterSystemApp; + + AccessControl({ + this.isFilterSystemApp = true, + this.mode = AccessControlMode.rejectSelected, + this.acceptList = const [], + this.rejectList = const [], + }); + + @JsonKey(includeFromJson: false, includeToJson: false) + List get currentList => + mode == AccessControlMode.acceptSelected ? acceptList : rejectList; + + set currentList(List currentList) { + if (mode == AccessControlMode.acceptSelected) { + acceptList = currentList; + } else { + rejectList = currentList; + } + } + + AccessControl copyWith({ + AccessControlMode? mode, + List? acceptList, + List? rejectList, + bool? isFilterSystemApp, + }) { + return AccessControl( + mode: mode ?? this.mode, + acceptList: acceptList ?? this.acceptList, + rejectList: rejectList ?? this.rejectList, + isFilterSystemApp: isFilterSystemApp ?? this.isFilterSystemApp, + ); + } + + Map toJson() { + return _$AccessControlToJson(this); + } + + factory AccessControl.fromJson(Map json) { + return _$AccessControlFromJson(json); + } +} + +@JsonSerializable() +class Config extends ChangeNotifier { + List _profiles; + String? _currentProfileId; + bool _autoLaunch; + bool _silentLaunch; + bool _autoRun; + bool _openLog; + ThemeMode _themeMode; + String? _locale; + int? _primaryColor; + ProxiesSortType _proxiesSortType; + bool _isMinimizeOnExit; + bool _isAccessControl; + AccessControl _accessControl; + bool _isAnimateToPage; + + Config() + : _profiles = [], + _autoLaunch = false, + _silentLaunch = false, + _autoRun = false, + _themeMode = ThemeMode.system, + _openLog = false, + _primaryColor = appConstant.defaultPrimaryColor.value, + _proxiesSortType = ProxiesSortType.none, + _isMinimizeOnExit = true, + _isAccessControl = false, + _accessControl = AccessControl(), + _isAnimateToPage = false; + + deleteProfileById(String id) { + _profiles = profiles.where((element) => element.id != id).toList(); + notifyListeners(); + } + + Profile? getCurrentProfileForId(String? value) { + if (value == null) { + return null; + } + return _profiles.firstWhere((element) => element.id == value); + } + + Profile? getCurrentProfile() { + return getCurrentProfileForId(_currentProfileId); + } + + String? _getLabel(String? label, String id) { + final hasDup = _profiles.indexWhere( + (element) => element.label == label && element.id != id) != + -1; + if (hasDup) { + return _getLabel(Other.getOverwriteLabel(label!), id); + } else { + return label; + } + } + + setProfile(Profile profile) { + final List profilesTemp = List.from(_profiles); + final index = + profilesTemp.indexWhere((element) => element.id == profile.id); + final updateProfile = profile.copyWith( + label: _getLabel(profile.label, profile.id), + ); + if (index == -1) { + profilesTemp.add(updateProfile); + } else { + profilesTemp[index] = updateProfile; + } + _profiles = profilesTemp; + notifyListeners(); + } + + @JsonKey(defaultValue: []) + List get profiles => _profiles; + + set profiles(List value) { + if (_profiles != value) { + _profiles = value; + notifyListeners(); + } + } + + String? get currentProfileId => _currentProfileId; + + set currentProfileId(String? value) { + if (_currentProfileId != value) { + _currentProfileId = value; + notifyListeners(); + } + } + + Profile? get currentProfile { + try { + return profiles.firstWhere((element) => element.id == _currentProfileId); + } catch (_) { + return null; + } + } + + String? get currentProxyName => currentProfile?.proxyName; + + String? get currentGroupName => currentProfile?.groupName; + + @JsonKey(defaultValue: false) + bool get autoLaunch { + if (!system.isDesktop) return false; + return _autoLaunch; + } + + set autoLaunch(bool value) { + if (_autoLaunch != value) { + _autoLaunch = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: false) + bool get silentLaunch => _silentLaunch; + + set silentLaunch(bool value) { + if (_silentLaunch != value) { + _silentLaunch = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: false) + bool get autoRun => _autoRun; + + set autoRun(bool value) { + if (_autoRun != value) { + _autoRun = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: ThemeMode.system) + ThemeMode get themeMode => _themeMode; + + set themeMode(ThemeMode value) { + if (_themeMode != value) { + _themeMode = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: false) + bool get openLogs => _openLog; + + set openLogs(bool value) { + if (_openLog != value) { + _openLog = value; + notifyListeners(); + } + } + + String? get locale => _locale; + + set locale(String? value) { + if (_locale != value) { + _locale = value; + notifyListeners(); + } + } + + int? get primaryColor => _primaryColor; + + set primaryColor(int? value) { + if (_primaryColor != value) { + _primaryColor = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: ProxiesSortType.none) + ProxiesSortType get proxiesSortType => _proxiesSortType; + + set proxiesSortType(ProxiesSortType value) { + if (_proxiesSortType != value) { + _proxiesSortType = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: true) + bool get isMinimizeOnExit => _isMinimizeOnExit; + + set isMinimizeOnExit(bool value) { + if (_isMinimizeOnExit != value) { + _isMinimizeOnExit = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: false) + bool get isAccessControl { + if (!Platform.isAndroid) return false; + return _isAccessControl; + } + + set isAccessControl(bool value) { + if (_isAccessControl != value) { + _isAccessControl = value; + notifyListeners(); + } + } + + AccessControl get accessControl => _accessControl; + + set accessControl(AccessControl? value) { + if (_accessControl != value) { + _accessControl = value ?? AccessControl(); + notifyListeners(); + } + } + + @JsonKey(defaultValue: true) + bool get isAnimateToPage { + if (!Platform.isAndroid) return false; + return _isAnimateToPage; + } + + set isAnimateToPage(bool value) { + if (_isAnimateToPage != value) { + _isAnimateToPage = value; + notifyListeners(); + } + } + + update() { + notifyListeners(); + } + + Map toJson() { + return _$ConfigToJson(this); + } + + factory Config.fromJson(Map json) { + return _$ConfigFromJson(json); + } +} diff --git a/lib/models/connection.dart b/lib/models/connection.dart new file mode 100644 index 0000000..a316011 --- /dev/null +++ b/lib/models/connection.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/connection.g.dart'; + +part 'generated/connection.freezed.dart'; + +@freezed +class Metadata with _$Metadata { + const factory Metadata({ + required int uid, + required String network, + required String sourceIP, + required String sourcePort, + required String destinationIP, + required String destinationPort, + required String host, + required String remoteDestination, + }) = _Metadata; + + factory Metadata.fromJson(Map json) => + _$MetadataFromJson(json); +} + +@freezed +class Connection with _$Connection{ + const factory Connection({ + required String id, + num? upload, + num? download, + required DateTime start, + required Metadata metadata, + required List chains, + }) = _Connection; + + factory Connection.fromJson(Map json) => + _$ConnectionFromJson(json); +} diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart new file mode 100644 index 0000000..241aa19 --- /dev/null +++ b/lib/models/ffi.dart @@ -0,0 +1,66 @@ +// ignore_for_file: invalid_annotation_target + +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/clash_config.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/ffi.g.dart'; + +part 'generated/ffi.freezed.dart'; + +@freezed +class UpdateConfigParams with _$UpdateConfigParams { + const factory UpdateConfigParams({ + @JsonKey(name: "profile-path") String? profilePath, + required ClashConfig config, + @JsonKey(name: "is-patch") bool? isPatch, + }) = _UpdateConfigParams; + + factory UpdateConfigParams.fromJson(Map json) => + _$UpdateConfigParamsFromJson(json); +} + +@freezed +class ChangeProxyParams with _$ChangeProxyParams { + const factory ChangeProxyParams({ + @JsonKey(name: "group-name") required String groupName, + @JsonKey(name: "proxy-name") required String proxyName, + }) = _ChangeProxyParams; + + factory ChangeProxyParams.fromJson(Map json) => + _$ChangeProxyParamsFromJson(json); +} + +@freezed +class Message with _$Message { + const factory Message({ + required MessageType type, + dynamic data, + }) = _Message; + + factory Message.fromJson(Map json) => + _$MessageFromJson(json); +} + +@freezed +class Delay with _$Delay { + const factory Delay({ + required String name, + int? value, + }) = _Delay; + + factory Delay.fromJson(Map json) => _$DelayFromJson(json); +} + +@freezed +class Process with _$Process { + const factory Process({ + required int uid, + required String network, + required String source, + required String target, + }) = _Process; + + factory Process.fromJson(Map json) => + _$ProcessFromJson(json); +} diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart new file mode 100644 index 0000000..0e612c5 --- /dev/null +++ b/lib/models/generated/clash_config.g.dart @@ -0,0 +1,96 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../clash_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Tun _$TunFromJson(Map json) => Tun() + ..enable = json['enable'] as bool + ..device = json['device'] as String + ..stack = $enumDecode(_$TunStackEnumMap, json['stack']) + ..dnsHijack = + (json['dns-hijack'] as List).map((e) => e as String).toList(); + +Map _$TunToJson(Tun instance) => { + 'enable': instance.enable, + 'device': instance.device, + 'stack': _$TunStackEnumMap[instance.stack]!, + 'dns-hijack': instance.dnsHijack, + }; + +const _$TunStackEnumMap = { + TunStack.gvisor: 'gvisor', + TunStack.system: 'system', + TunStack.mixed: 'mixed', +}; + +Dns _$DnsFromJson(Map json) => Dns() + ..enable = json['enable'] as bool + ..ipv6 = json['ipv6'] as bool + ..defaultNameserver = (json['default-nameserver'] as List) + .map((e) => e as String) + .toList() + ..enhancedMode = json['enhanced-mode'] as String + ..fakeIpRange = json['fake-ip-range'] as String + ..useHosts = json['use-hosts'] as bool + ..nameserver = + (json['nameserver'] as List).map((e) => e as String).toList() + ..fallback = + (json['fallback'] as List).map((e) => e as String).toList() + ..fakeIpFilter = (json['fake-ip-filter'] as List) + .map((e) => e as String) + .toList(); + +Map _$DnsToJson(Dns instance) => { + 'enable': instance.enable, + 'ipv6': instance.ipv6, + 'default-nameserver': instance.defaultNameserver, + 'enhanced-mode': instance.enhancedMode, + 'fake-ip-range': instance.fakeIpRange, + 'use-hosts': instance.useHosts, + 'nameserver': instance.nameserver, + 'fallback': instance.fallback, + 'fake-ip-filter': instance.fakeIpFilter, + }; + +ClashConfig _$ClashConfigFromJson(Map json) => ClashConfig( + mixedPort: (json['mixed-port'] as num?)?.toInt(), + mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']), + allowLan: json['allow-lan'] as bool?, + logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']), + tun: json['tun'] == null + ? null + : Tun.fromJson(json['tun'] as Map), + dns: json['dns'] == null + ? null + : Dns.fromJson(json['dns'] as Map), + rules: + (json['rules'] as List?)?.map((e) => e as String).toList(), + ); + +Map _$ClashConfigToJson(ClashConfig instance) => + { + 'mixed-port': instance.mixedPort, + 'mode': _$ModeEnumMap[instance.mode]!, + 'allow-lan': instance.allowLan, + 'log-level': _$LogLevelEnumMap[instance.logLevel]!, + 'tun': instance.tun, + 'dns': instance.dns, + 'rules': instance.rules, + }; + +const _$ModeEnumMap = { + Mode.rule: 'rule', + Mode.global: 'global', + Mode.direct: 'direct', +}; + +const _$LogLevelEnumMap = { + LogLevel.debug: 'debug', + LogLevel.info: 'info', + LogLevel.warning: 'warning', + LogLevel.error: 'error', + LogLevel.silent: 'silent', +}; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart new file mode 100644 index 0000000..1701e47 --- /dev/null +++ b/lib/models/generated/config.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AccessControl _$AccessControlFromJson(Map json) => + AccessControl( + isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true, + mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ?? + AccessControlMode.rejectSelected, + acceptList: (json['acceptList'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + rejectList: (json['rejectList'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$AccessControlToJson(AccessControl instance) => + { + 'mode': _$AccessControlModeEnumMap[instance.mode]!, + 'acceptList': instance.acceptList, + 'rejectList': instance.rejectList, + 'isFilterSystemApp': instance.isFilterSystemApp, + }; + +const _$AccessControlModeEnumMap = { + AccessControlMode.acceptSelected: 'acceptSelected', + AccessControlMode.rejectSelected: 'rejectSelected', +}; + +Config _$ConfigFromJson(Map json) => Config() + ..profiles = (json['profiles'] as List?) + ?.map((e) => Profile.fromJson(e as Map)) + .toList() ?? + [] + ..currentProfileId = json['currentProfileId'] as String? + ..autoLaunch = json['autoLaunch'] as bool? ?? false + ..silentLaunch = json['silentLaunch'] as bool? ?? false + ..autoRun = json['autoRun'] as bool? ?? false + ..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? + ThemeMode.system + ..openLogs = json['openLogs'] as bool? ?? false + ..locale = json['locale'] as String? + ..primaryColor = (json['primaryColor'] as num?)?.toInt() + ..proxiesSortType = + $enumDecodeNullable(_$ProxiesSortTypeEnumMap, json['proxiesSortType']) ?? + ProxiesSortType.none + ..isMinimizeOnExit = json['isMinimizeOnExit'] as bool? ?? true + ..isAccessControl = json['isAccessControl'] as bool? ?? false + ..accessControl = + AccessControl.fromJson(json['accessControl'] as Map) + ..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true; + +Map _$ConfigToJson(Config instance) => { + 'profiles': instance.profiles, + 'currentProfileId': instance.currentProfileId, + 'autoLaunch': instance.autoLaunch, + 'silentLaunch': instance.silentLaunch, + 'autoRun': instance.autoRun, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'openLogs': instance.openLogs, + 'locale': instance.locale, + 'primaryColor': instance.primaryColor, + 'proxiesSortType': _$ProxiesSortTypeEnumMap[instance.proxiesSortType]!, + 'isMinimizeOnExit': instance.isMinimizeOnExit, + 'isAccessControl': instance.isAccessControl, + 'accessControl': instance.accessControl, + 'isAnimateToPage': instance.isAnimateToPage, + }; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$ProxiesSortTypeEnumMap = { + ProxiesSortType.none: 'none', + ProxiesSortType.delay: 'delay', + ProxiesSortType.name: 'name', +}; diff --git a/lib/models/generated/connection.freezed.dart b/lib/models/generated/connection.freezed.dart new file mode 100644 index 0000000..da1dcf4 --- /dev/null +++ b/lib/models/generated/connection.freezed.dart @@ -0,0 +1,562 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../connection.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Metadata _$MetadataFromJson(Map json) { + return _Metadata.fromJson(json); +} + +/// @nodoc +mixin _$Metadata { + int get uid => throw _privateConstructorUsedError; + String get network => throw _privateConstructorUsedError; + String get sourceIP => throw _privateConstructorUsedError; + String get sourcePort => throw _privateConstructorUsedError; + String get destinationIP => throw _privateConstructorUsedError; + String get destinationPort => throw _privateConstructorUsedError; + String get host => throw _privateConstructorUsedError; + String get remoteDestination => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MetadataCopyWith<$Res> { + factory $MetadataCopyWith(Metadata value, $Res Function(Metadata) then) = + _$MetadataCopyWithImpl<$Res, Metadata>; + @useResult + $Res call( + {int uid, + String network, + String sourceIP, + String sourcePort, + String destinationIP, + String destinationPort, + String host, + String remoteDestination}); +} + +/// @nodoc +class _$MetadataCopyWithImpl<$Res, $Val extends Metadata> + implements $MetadataCopyWith<$Res> { + _$MetadataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uid = null, + Object? network = null, + Object? sourceIP = null, + Object? sourcePort = null, + Object? destinationIP = null, + Object? destinationPort = null, + Object? host = null, + Object? remoteDestination = null, + }) { + return _then(_value.copyWith( + uid: null == uid + ? _value.uid + : uid // ignore: cast_nullable_to_non_nullable + as int, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + sourceIP: null == sourceIP + ? _value.sourceIP + : sourceIP // ignore: cast_nullable_to_non_nullable + as String, + sourcePort: null == sourcePort + ? _value.sourcePort + : sourcePort // ignore: cast_nullable_to_non_nullable + as String, + destinationIP: null == destinationIP + ? _value.destinationIP + : destinationIP // ignore: cast_nullable_to_non_nullable + as String, + destinationPort: null == destinationPort + ? _value.destinationPort + : destinationPort // ignore: cast_nullable_to_non_nullable + as String, + host: null == host + ? _value.host + : host // ignore: cast_nullable_to_non_nullable + as String, + remoteDestination: null == remoteDestination + ? _value.remoteDestination + : remoteDestination // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MetadataImplCopyWith<$Res> + implements $MetadataCopyWith<$Res> { + factory _$$MetadataImplCopyWith( + _$MetadataImpl value, $Res Function(_$MetadataImpl) then) = + __$$MetadataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int uid, + String network, + String sourceIP, + String sourcePort, + String destinationIP, + String destinationPort, + String host, + String remoteDestination}); +} + +/// @nodoc +class __$$MetadataImplCopyWithImpl<$Res> + extends _$MetadataCopyWithImpl<$Res, _$MetadataImpl> + implements _$$MetadataImplCopyWith<$Res> { + __$$MetadataImplCopyWithImpl( + _$MetadataImpl _value, $Res Function(_$MetadataImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uid = null, + Object? network = null, + Object? sourceIP = null, + Object? sourcePort = null, + Object? destinationIP = null, + Object? destinationPort = null, + Object? host = null, + Object? remoteDestination = null, + }) { + return _then(_$MetadataImpl( + uid: null == uid + ? _value.uid + : uid // ignore: cast_nullable_to_non_nullable + as int, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + sourceIP: null == sourceIP + ? _value.sourceIP + : sourceIP // ignore: cast_nullable_to_non_nullable + as String, + sourcePort: null == sourcePort + ? _value.sourcePort + : sourcePort // ignore: cast_nullable_to_non_nullable + as String, + destinationIP: null == destinationIP + ? _value.destinationIP + : destinationIP // ignore: cast_nullable_to_non_nullable + as String, + destinationPort: null == destinationPort + ? _value.destinationPort + : destinationPort // ignore: cast_nullable_to_non_nullable + as String, + host: null == host + ? _value.host + : host // ignore: cast_nullable_to_non_nullable + as String, + remoteDestination: null == remoteDestination + ? _value.remoteDestination + : remoteDestination // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MetadataImpl implements _Metadata { + const _$MetadataImpl( + {required this.uid, + required this.network, + required this.sourceIP, + required this.sourcePort, + required this.destinationIP, + required this.destinationPort, + required this.host, + required this.remoteDestination}); + + factory _$MetadataImpl.fromJson(Map json) => + _$$MetadataImplFromJson(json); + + @override + final int uid; + @override + final String network; + @override + final String sourceIP; + @override + final String sourcePort; + @override + final String destinationIP; + @override + final String destinationPort; + @override + final String host; + @override + final String remoteDestination; + + @override + String toString() { + return 'Metadata(uid: $uid, network: $network, sourceIP: $sourceIP, sourcePort: $sourcePort, destinationIP: $destinationIP, destinationPort: $destinationPort, host: $host, remoteDestination: $remoteDestination)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MetadataImpl && + (identical(other.uid, uid) || other.uid == uid) && + (identical(other.network, network) || other.network == network) && + (identical(other.sourceIP, sourceIP) || + other.sourceIP == sourceIP) && + (identical(other.sourcePort, sourcePort) || + other.sourcePort == sourcePort) && + (identical(other.destinationIP, destinationIP) || + other.destinationIP == destinationIP) && + (identical(other.destinationPort, destinationPort) || + other.destinationPort == destinationPort) && + (identical(other.host, host) || other.host == host) && + (identical(other.remoteDestination, remoteDestination) || + other.remoteDestination == remoteDestination)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, uid, network, sourceIP, + sourcePort, destinationIP, destinationPort, host, remoteDestination); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MetadataImplCopyWith<_$MetadataImpl> get copyWith => + __$$MetadataImplCopyWithImpl<_$MetadataImpl>(this, _$identity); + + @override + Map toJson() { + return _$$MetadataImplToJson( + this, + ); + } +} + +abstract class _Metadata implements Metadata { + const factory _Metadata( + {required final int uid, + required final String network, + required final String sourceIP, + required final String sourcePort, + required final String destinationIP, + required final String destinationPort, + required final String host, + required final String remoteDestination}) = _$MetadataImpl; + + factory _Metadata.fromJson(Map json) = + _$MetadataImpl.fromJson; + + @override + int get uid; + @override + String get network; + @override + String get sourceIP; + @override + String get sourcePort; + @override + String get destinationIP; + @override + String get destinationPort; + @override + String get host; + @override + String get remoteDestination; + @override + @JsonKey(ignore: true) + _$$MetadataImplCopyWith<_$MetadataImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Connection _$ConnectionFromJson(Map json) { + return _Connection.fromJson(json); +} + +/// @nodoc +mixin _$Connection { + String get id => throw _privateConstructorUsedError; + num? get upload => throw _privateConstructorUsedError; + num? get download => throw _privateConstructorUsedError; + DateTime get start => throw _privateConstructorUsedError; + Metadata get metadata => throw _privateConstructorUsedError; + List get chains => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ConnectionCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConnectionCopyWith<$Res> { + factory $ConnectionCopyWith( + Connection value, $Res Function(Connection) then) = + _$ConnectionCopyWithImpl<$Res, Connection>; + @useResult + $Res call( + {String id, + num? upload, + num? download, + DateTime start, + Metadata metadata, + List chains}); + + $MetadataCopyWith<$Res> get metadata; +} + +/// @nodoc +class _$ConnectionCopyWithImpl<$Res, $Val extends Connection> + implements $ConnectionCopyWith<$Res> { + _$ConnectionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? upload = freezed, + Object? download = freezed, + Object? start = null, + Object? metadata = null, + Object? chains = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + upload: freezed == upload + ? _value.upload + : upload // ignore: cast_nullable_to_non_nullable + as num?, + download: freezed == download + ? _value.download + : download // ignore: cast_nullable_to_non_nullable + as num?, + start: null == start + ? _value.start + : start // ignore: cast_nullable_to_non_nullable + as DateTime, + metadata: null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Metadata, + chains: null == chains + ? _value.chains + : chains // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $MetadataCopyWith<$Res> get metadata { + return $MetadataCopyWith<$Res>(_value.metadata, (value) { + return _then(_value.copyWith(metadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ConnectionImplCopyWith<$Res> + implements $ConnectionCopyWith<$Res> { + factory _$$ConnectionImplCopyWith( + _$ConnectionImpl value, $Res Function(_$ConnectionImpl) then) = + __$$ConnectionImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + num? upload, + num? download, + DateTime start, + Metadata metadata, + List chains}); + + @override + $MetadataCopyWith<$Res> get metadata; +} + +/// @nodoc +class __$$ConnectionImplCopyWithImpl<$Res> + extends _$ConnectionCopyWithImpl<$Res, _$ConnectionImpl> + implements _$$ConnectionImplCopyWith<$Res> { + __$$ConnectionImplCopyWithImpl( + _$ConnectionImpl _value, $Res Function(_$ConnectionImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? upload = freezed, + Object? download = freezed, + Object? start = null, + Object? metadata = null, + Object? chains = null, + }) { + return _then(_$ConnectionImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + upload: freezed == upload + ? _value.upload + : upload // ignore: cast_nullable_to_non_nullable + as num?, + download: freezed == download + ? _value.download + : download // ignore: cast_nullable_to_non_nullable + as num?, + start: null == start + ? _value.start + : start // ignore: cast_nullable_to_non_nullable + as DateTime, + metadata: null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Metadata, + chains: null == chains + ? _value._chains + : chains // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConnectionImpl implements _Connection { + const _$ConnectionImpl( + {required this.id, + this.upload, + this.download, + required this.start, + required this.metadata, + required final List chains}) + : _chains = chains; + + factory _$ConnectionImpl.fromJson(Map json) => + _$$ConnectionImplFromJson(json); + + @override + final String id; + @override + final num? upload; + @override + final num? download; + @override + final DateTime start; + @override + final Metadata metadata; + final List _chains; + @override + List get chains { + if (_chains is EqualUnmodifiableListView) return _chains; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_chains); + } + + @override + String toString() { + return 'Connection(id: $id, upload: $upload, download: $download, start: $start, metadata: $metadata, chains: $chains)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConnectionImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.upload, upload) || other.upload == upload) && + (identical(other.download, download) || + other.download == download) && + (identical(other.start, start) || other.start == start) && + (identical(other.metadata, metadata) || + other.metadata == metadata) && + const DeepCollectionEquality().equals(other._chains, _chains)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, id, upload, download, start, + metadata, const DeepCollectionEquality().hash(_chains)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ConnectionImplCopyWith<_$ConnectionImpl> get copyWith => + __$$ConnectionImplCopyWithImpl<_$ConnectionImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ConnectionImplToJson( + this, + ); + } +} + +abstract class _Connection implements Connection { + const factory _Connection( + {required final String id, + final num? upload, + final num? download, + required final DateTime start, + required final Metadata metadata, + required final List chains}) = _$ConnectionImpl; + + factory _Connection.fromJson(Map json) = + _$ConnectionImpl.fromJson; + + @override + String get id; + @override + num? get upload; + @override + num? get download; + @override + DateTime get start; + @override + Metadata get metadata; + @override + List get chains; + @override + @JsonKey(ignore: true) + _$$ConnectionImplCopyWith<_$ConnectionImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/connection.g.dart b/lib/models/generated/connection.g.dart new file mode 100644 index 0000000..0397065 --- /dev/null +++ b/lib/models/generated/connection.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../connection.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MetadataImpl _$$MetadataImplFromJson(Map json) => + _$MetadataImpl( + uid: (json['uid'] as num).toInt(), + network: json['network'] as String, + sourceIP: json['sourceIP'] as String, + sourcePort: json['sourcePort'] as String, + destinationIP: json['destinationIP'] as String, + destinationPort: json['destinationPort'] as String, + host: json['host'] as String, + remoteDestination: json['remoteDestination'] as String, + ); + +Map _$$MetadataImplToJson(_$MetadataImpl instance) => + { + 'uid': instance.uid, + 'network': instance.network, + 'sourceIP': instance.sourceIP, + 'sourcePort': instance.sourcePort, + 'destinationIP': instance.destinationIP, + 'destinationPort': instance.destinationPort, + 'host': instance.host, + 'remoteDestination': instance.remoteDestination, + }; + +_$ConnectionImpl _$$ConnectionImplFromJson(Map json) => + _$ConnectionImpl( + id: json['id'] as String, + upload: json['upload'] as num?, + download: json['download'] as num?, + start: DateTime.parse(json['start'] as String), + metadata: Metadata.fromJson(json['metadata'] as Map), + chains: + (json['chains'] as List).map((e) => e as String).toList(), + ); + +Map _$$ConnectionImplToJson(_$ConnectionImpl instance) => + { + 'id': instance.id, + 'upload': instance.upload, + 'download': instance.download, + 'start': instance.start.toIso8601String(), + 'metadata': instance.metadata, + 'chains': instance.chains, + }; diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/ffi.freezed.dart new file mode 100644 index 0000000..55862e8 --- /dev/null +++ b/lib/models/generated/ffi.freezed.dart @@ -0,0 +1,861 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../ffi.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +UpdateConfigParams _$UpdateConfigParamsFromJson(Map json) { + return _UpdateConfigParams.fromJson(json); +} + +/// @nodoc +mixin _$UpdateConfigParams { + @JsonKey(name: "profile-path") + String? get profilePath => throw _privateConstructorUsedError; + ClashConfig get config => throw _privateConstructorUsedError; + @JsonKey(name: "is-patch") + bool? get isPatch => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UpdateConfigParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateConfigParamsCopyWith<$Res> { + factory $UpdateConfigParamsCopyWith( + UpdateConfigParams value, $Res Function(UpdateConfigParams) then) = + _$UpdateConfigParamsCopyWithImpl<$Res, UpdateConfigParams>; + @useResult + $Res call( + {@JsonKey(name: "profile-path") String? profilePath, + ClashConfig config, + @JsonKey(name: "is-patch") bool? isPatch}); +} + +/// @nodoc +class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams> + implements $UpdateConfigParamsCopyWith<$Res> { + _$UpdateConfigParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profilePath = freezed, + Object? config = null, + Object? isPatch = freezed, + }) { + return _then(_value.copyWith( + profilePath: freezed == profilePath + ? _value.profilePath + : profilePath // ignore: cast_nullable_to_non_nullable + as String?, + config: null == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as ClashConfig, + isPatch: freezed == isPatch + ? _value.isPatch + : isPatch // ignore: cast_nullable_to_non_nullable + as bool?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UpdateConfigParamsImplCopyWith<$Res> + implements $UpdateConfigParamsCopyWith<$Res> { + factory _$$UpdateConfigParamsImplCopyWith(_$UpdateConfigParamsImpl value, + $Res Function(_$UpdateConfigParamsImpl) then) = + __$$UpdateConfigParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: "profile-path") String? profilePath, + ClashConfig config, + @JsonKey(name: "is-patch") bool? isPatch}); +} + +/// @nodoc +class __$$UpdateConfigParamsImplCopyWithImpl<$Res> + extends _$UpdateConfigParamsCopyWithImpl<$Res, _$UpdateConfigParamsImpl> + implements _$$UpdateConfigParamsImplCopyWith<$Res> { + __$$UpdateConfigParamsImplCopyWithImpl(_$UpdateConfigParamsImpl _value, + $Res Function(_$UpdateConfigParamsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profilePath = freezed, + Object? config = null, + Object? isPatch = freezed, + }) { + return _then(_$UpdateConfigParamsImpl( + profilePath: freezed == profilePath + ? _value.profilePath + : profilePath // ignore: cast_nullable_to_non_nullable + as String?, + config: null == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as ClashConfig, + isPatch: freezed == isPatch + ? _value.isPatch + : isPatch // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateConfigParamsImpl implements _UpdateConfigParams { + const _$UpdateConfigParamsImpl( + {@JsonKey(name: "profile-path") this.profilePath, + required this.config, + @JsonKey(name: "is-patch") this.isPatch}); + + factory _$UpdateConfigParamsImpl.fromJson(Map json) => + _$$UpdateConfigParamsImplFromJson(json); + + @override + @JsonKey(name: "profile-path") + final String? profilePath; + @override + final ClashConfig config; + @override + @JsonKey(name: "is-patch") + final bool? isPatch; + + @override + String toString() { + return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateConfigParamsImpl && + (identical(other.profilePath, profilePath) || + other.profilePath == profilePath) && + (identical(other.config, config) || other.config == config) && + (identical(other.isPatch, isPatch) || other.isPatch == isPatch)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, profilePath, config, isPatch); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith => + __$$UpdateConfigParamsImplCopyWithImpl<_$UpdateConfigParamsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UpdateConfigParamsImplToJson( + this, + ); + } +} + +abstract class _UpdateConfigParams implements UpdateConfigParams { + const factory _UpdateConfigParams( + {@JsonKey(name: "profile-path") final String? profilePath, + required final ClashConfig config, + @JsonKey(name: "is-patch") final bool? isPatch}) = + _$UpdateConfigParamsImpl; + + factory _UpdateConfigParams.fromJson(Map json) = + _$UpdateConfigParamsImpl.fromJson; + + @override + @JsonKey(name: "profile-path") + String? get profilePath; + @override + ClashConfig get config; + @override + @JsonKey(name: "is-patch") + bool? get isPatch; + @override + @JsonKey(ignore: true) + _$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ChangeProxyParams _$ChangeProxyParamsFromJson(Map json) { + return _ChangeProxyParams.fromJson(json); +} + +/// @nodoc +mixin _$ChangeProxyParams { + @JsonKey(name: "group-name") + String get groupName => throw _privateConstructorUsedError; + @JsonKey(name: "proxy-name") + String get proxyName => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ChangeProxyParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChangeProxyParamsCopyWith<$Res> { + factory $ChangeProxyParamsCopyWith( + ChangeProxyParams value, $Res Function(ChangeProxyParams) then) = + _$ChangeProxyParamsCopyWithImpl<$Res, ChangeProxyParams>; + @useResult + $Res call( + {@JsonKey(name: "group-name") String groupName, + @JsonKey(name: "proxy-name") String proxyName}); +} + +/// @nodoc +class _$ChangeProxyParamsCopyWithImpl<$Res, $Val extends ChangeProxyParams> + implements $ChangeProxyParamsCopyWith<$Res> { + _$ChangeProxyParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? groupName = null, + Object? proxyName = null, + }) { + return _then(_value.copyWith( + groupName: null == groupName + ? _value.groupName + : groupName // ignore: cast_nullable_to_non_nullable + as String, + proxyName: null == proxyName + ? _value.proxyName + : proxyName // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChangeProxyParamsImplCopyWith<$Res> + implements $ChangeProxyParamsCopyWith<$Res> { + factory _$$ChangeProxyParamsImplCopyWith(_$ChangeProxyParamsImpl value, + $Res Function(_$ChangeProxyParamsImpl) then) = + __$$ChangeProxyParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: "group-name") String groupName, + @JsonKey(name: "proxy-name") String proxyName}); +} + +/// @nodoc +class __$$ChangeProxyParamsImplCopyWithImpl<$Res> + extends _$ChangeProxyParamsCopyWithImpl<$Res, _$ChangeProxyParamsImpl> + implements _$$ChangeProxyParamsImplCopyWith<$Res> { + __$$ChangeProxyParamsImplCopyWithImpl(_$ChangeProxyParamsImpl _value, + $Res Function(_$ChangeProxyParamsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? groupName = null, + Object? proxyName = null, + }) { + return _then(_$ChangeProxyParamsImpl( + groupName: null == groupName + ? _value.groupName + : groupName // ignore: cast_nullable_to_non_nullable + as String, + proxyName: null == proxyName + ? _value.proxyName + : proxyName // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChangeProxyParamsImpl implements _ChangeProxyParams { + const _$ChangeProxyParamsImpl( + {@JsonKey(name: "group-name") required this.groupName, + @JsonKey(name: "proxy-name") required this.proxyName}); + + factory _$ChangeProxyParamsImpl.fromJson(Map json) => + _$$ChangeProxyParamsImplFromJson(json); + + @override + @JsonKey(name: "group-name") + final String groupName; + @override + @JsonKey(name: "proxy-name") + final String proxyName; + + @override + String toString() { + return 'ChangeProxyParams(groupName: $groupName, proxyName: $proxyName)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChangeProxyParamsImpl && + (identical(other.groupName, groupName) || + other.groupName == groupName) && + (identical(other.proxyName, proxyName) || + other.proxyName == proxyName)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, groupName, proxyName); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChangeProxyParamsImplCopyWith<_$ChangeProxyParamsImpl> get copyWith => + __$$ChangeProxyParamsImplCopyWithImpl<_$ChangeProxyParamsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ChangeProxyParamsImplToJson( + this, + ); + } +} + +abstract class _ChangeProxyParams implements ChangeProxyParams { + const factory _ChangeProxyParams( + {@JsonKey(name: "group-name") required final String groupName, + @JsonKey(name: "proxy-name") required final String proxyName}) = + _$ChangeProxyParamsImpl; + + factory _ChangeProxyParams.fromJson(Map json) = + _$ChangeProxyParamsImpl.fromJson; + + @override + @JsonKey(name: "group-name") + String get groupName; + @override + @JsonKey(name: "proxy-name") + String get proxyName; + @override + @JsonKey(ignore: true) + _$$ChangeProxyParamsImplCopyWith<_$ChangeProxyParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Message _$MessageFromJson(Map json) { + return _Message.fromJson(json); +} + +/// @nodoc +mixin _$Message { + MessageType get type => throw _privateConstructorUsedError; + dynamic get data => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MessageCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MessageCopyWith<$Res> { + factory $MessageCopyWith(Message value, $Res Function(Message) then) = + _$MessageCopyWithImpl<$Res, Message>; + @useResult + $Res call({MessageType type, dynamic data}); +} + +/// @nodoc +class _$MessageCopyWithImpl<$Res, $Val extends Message> + implements $MessageCopyWith<$Res> { + _$MessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? data = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as MessageType, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> { + factory _$$MessageImplCopyWith( + _$MessageImpl value, $Res Function(_$MessageImpl) then) = + __$$MessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({MessageType type, dynamic data}); +} + +/// @nodoc +class __$$MessageImplCopyWithImpl<$Res> + extends _$MessageCopyWithImpl<$Res, _$MessageImpl> + implements _$$MessageImplCopyWith<$Res> { + __$$MessageImplCopyWithImpl( + _$MessageImpl _value, $Res Function(_$MessageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? data = freezed, + }) { + return _then(_$MessageImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as MessageType, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MessageImpl implements _Message { + const _$MessageImpl({required this.type, this.data}); + + factory _$MessageImpl.fromJson(Map json) => + _$$MessageImplFromJson(json); + + @override + final MessageType type; + @override + final dynamic data; + + @override + String toString() { + return 'Message(type: $type, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MessageImpl && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other.data, data)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, type, const DeepCollectionEquality().hash(data)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MessageImplCopyWith<_$MessageImpl> get copyWith => + __$$MessageImplCopyWithImpl<_$MessageImpl>(this, _$identity); + + @override + Map toJson() { + return _$$MessageImplToJson( + this, + ); + } +} + +abstract class _Message implements Message { + const factory _Message( + {required final MessageType type, final dynamic data}) = _$MessageImpl; + + factory _Message.fromJson(Map json) = _$MessageImpl.fromJson; + + @override + MessageType get type; + @override + dynamic get data; + @override + @JsonKey(ignore: true) + _$$MessageImplCopyWith<_$MessageImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Delay _$DelayFromJson(Map json) { + return _Delay.fromJson(json); +} + +/// @nodoc +mixin _$Delay { + String get name => throw _privateConstructorUsedError; + int? get value => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DelayCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DelayCopyWith<$Res> { + factory $DelayCopyWith(Delay value, $Res Function(Delay) then) = + _$DelayCopyWithImpl<$Res, Delay>; + @useResult + $Res call({String name, int? value}); +} + +/// @nodoc +class _$DelayCopyWithImpl<$Res, $Val extends Delay> + implements $DelayCopyWith<$Res> { + _$DelayCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? value = freezed, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DelayImplCopyWith<$Res> implements $DelayCopyWith<$Res> { + factory _$$DelayImplCopyWith( + _$DelayImpl value, $Res Function(_$DelayImpl) then) = + __$$DelayImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, int? value}); +} + +/// @nodoc +class __$$DelayImplCopyWithImpl<$Res> + extends _$DelayCopyWithImpl<$Res, _$DelayImpl> + implements _$$DelayImplCopyWith<$Res> { + __$$DelayImplCopyWithImpl( + _$DelayImpl _value, $Res Function(_$DelayImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? value = freezed, + }) { + return _then(_$DelayImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$DelayImpl implements _Delay { + const _$DelayImpl({required this.name, this.value}); + + factory _$DelayImpl.fromJson(Map json) => + _$$DelayImplFromJson(json); + + @override + final String name; + @override + final int? value; + + @override + String toString() { + return 'Delay(name: $name, value: $value)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DelayImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.value, value) || other.value == value)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, value); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DelayImplCopyWith<_$DelayImpl> get copyWith => + __$$DelayImplCopyWithImpl<_$DelayImpl>(this, _$identity); + + @override + Map toJson() { + return _$$DelayImplToJson( + this, + ); + } +} + +abstract class _Delay implements Delay { + const factory _Delay({required final String name, final int? value}) = + _$DelayImpl; + + factory _Delay.fromJson(Map json) = _$DelayImpl.fromJson; + + @override + String get name; + @override + int? get value; + @override + @JsonKey(ignore: true) + _$$DelayImplCopyWith<_$DelayImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Process _$ProcessFromJson(Map json) { + return _Process.fromJson(json); +} + +/// @nodoc +mixin _$Process { + int get uid => throw _privateConstructorUsedError; + String get network => throw _privateConstructorUsedError; + String get source => throw _privateConstructorUsedError; + String get target => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ProcessCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProcessCopyWith<$Res> { + factory $ProcessCopyWith(Process value, $Res Function(Process) then) = + _$ProcessCopyWithImpl<$Res, Process>; + @useResult + $Res call({int uid, String network, String source, String target}); +} + +/// @nodoc +class _$ProcessCopyWithImpl<$Res, $Val extends Process> + implements $ProcessCopyWith<$Res> { + _$ProcessCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uid = null, + Object? network = null, + Object? source = null, + Object? target = null, + }) { + return _then(_value.copyWith( + uid: null == uid + ? _value.uid + : uid // ignore: cast_nullable_to_non_nullable + as int, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + target: null == target + ? _value.target + : target // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> { + factory _$$ProcessImplCopyWith( + _$ProcessImpl value, $Res Function(_$ProcessImpl) then) = + __$$ProcessImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int uid, String network, String source, String target}); +} + +/// @nodoc +class __$$ProcessImplCopyWithImpl<$Res> + extends _$ProcessCopyWithImpl<$Res, _$ProcessImpl> + implements _$$ProcessImplCopyWith<$Res> { + __$$ProcessImplCopyWithImpl( + _$ProcessImpl _value, $Res Function(_$ProcessImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uid = null, + Object? network = null, + Object? source = null, + Object? target = null, + }) { + return _then(_$ProcessImpl( + uid: null == uid + ? _value.uid + : uid // ignore: cast_nullable_to_non_nullable + as int, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + target: null == target + ? _value.target + : target // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ProcessImpl implements _Process { + const _$ProcessImpl( + {required this.uid, + required this.network, + required this.source, + required this.target}); + + factory _$ProcessImpl.fromJson(Map json) => + _$$ProcessImplFromJson(json); + + @override + final int uid; + @override + final String network; + @override + final String source; + @override + final String target; + + @override + String toString() { + return 'Process(uid: $uid, network: $network, source: $source, target: $target)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProcessImpl && + (identical(other.uid, uid) || other.uid == uid) && + (identical(other.network, network) || other.network == network) && + (identical(other.source, source) || other.source == source) && + (identical(other.target, target) || other.target == target)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, uid, network, source, target); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProcessImplCopyWith<_$ProcessImpl> get copyWith => + __$$ProcessImplCopyWithImpl<_$ProcessImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ProcessImplToJson( + this, + ); + } +} + +abstract class _Process implements Process { + const factory _Process( + {required final int uid, + required final String network, + required final String source, + required final String target}) = _$ProcessImpl; + + factory _Process.fromJson(Map json) = _$ProcessImpl.fromJson; + + @override + int get uid; + @override + String get network; + @override + String get source; + @override + String get target; + @override + @JsonKey(ignore: true) + _$$ProcessImplCopyWith<_$ProcessImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/ffi.g.dart new file mode 100644 index 0000000..5e3fd98 --- /dev/null +++ b/lib/models/generated/ffi.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../ffi.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( + Map json) => + _$UpdateConfigParamsImpl( + profilePath: json['profile-path'] as String?, + config: ClashConfig.fromJson(json['config'] as Map), + isPatch: json['is-patch'] as bool?, + ); + +Map _$$UpdateConfigParamsImplToJson( + _$UpdateConfigParamsImpl instance) => + { + 'profile-path': instance.profilePath, + 'config': instance.config, + 'is-patch': instance.isPatch, + }; + +_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson( + Map json) => + _$ChangeProxyParamsImpl( + groupName: json['group-name'] as String, + proxyName: json['proxy-name'] as String, + ); + +Map _$$ChangeProxyParamsImplToJson( + _$ChangeProxyParamsImpl instance) => + { + 'group-name': instance.groupName, + 'proxy-name': instance.proxyName, + }; + +_$MessageImpl _$$MessageImplFromJson(Map json) => + _$MessageImpl( + type: $enumDecode(_$MessageTypeEnumMap, json['type']), + data: json['data'], + ); + +Map _$$MessageImplToJson(_$MessageImpl instance) => + { + 'type': _$MessageTypeEnumMap[instance.type]!, + 'data': instance.data, + }; + +const _$MessageTypeEnumMap = { + MessageType.log: 'log', + MessageType.tun: 'tun', + MessageType.delay: 'delay', + MessageType.process: 'process', +}; + +_$DelayImpl _$$DelayImplFromJson(Map json) => _$DelayImpl( + name: json['name'] as String, + value: (json['value'] as num?)?.toInt(), + ); + +Map _$$DelayImplToJson(_$DelayImpl instance) => + { + 'name': instance.name, + 'value': instance.value, + }; + +_$ProcessImpl _$$ProcessImplFromJson(Map json) => + _$ProcessImpl( + uid: (json['uid'] as num).toInt(), + network: json['network'] as String, + source: json['source'] as String, + target: json['target'] as String, + ); + +Map _$$ProcessImplToJson(_$ProcessImpl instance) => + { + 'uid': instance.uid, + 'network': instance.network, + 'source': instance.source, + 'target': instance.target, + }; diff --git a/lib/models/generated/log.g.dart b/lib/models/generated/log.g.dart new file mode 100644 index 0000000..5564e82 --- /dev/null +++ b/lib/models/generated/log.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../log.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Log _$LogFromJson(Map json) => Log( + logLevel: $enumDecode(_$LogLevelEnumMap, json['LogLevel']), + payload: json['Payload'] as String?, + ); + +Map _$LogToJson(Log instance) => { + 'LogLevel': _$LogLevelEnumMap[instance.logLevel]!, + 'Payload': instance.payload, + }; + +const _$LogLevelEnumMap = { + LogLevel.debug: 'debug', + LogLevel.info: 'info', + LogLevel.warning: 'warning', + LogLevel.error: 'error', + LogLevel.silent: 'silent', +}; diff --git a/lib/models/generated/navigation.freezed.dart b/lib/models/generated/navigation.freezed.dart new file mode 100644 index 0000000..859b790 --- /dev/null +++ b/lib/models/generated/navigation.freezed.dart @@ -0,0 +1,250 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../navigation.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$NavigationItem { + Icon get icon => throw _privateConstructorUsedError; + String get label => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + Widget get fragment => throw _privateConstructorUsedError; + String? get path => throw _privateConstructorUsedError; + List get modes => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $NavigationItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NavigationItemCopyWith<$Res> { + factory $NavigationItemCopyWith( + NavigationItem value, $Res Function(NavigationItem) then) = + _$NavigationItemCopyWithImpl<$Res, NavigationItem>; + @useResult + $Res call( + {Icon icon, + String label, + String? description, + Widget fragment, + String? path, + List modes}); +} + +/// @nodoc +class _$NavigationItemCopyWithImpl<$Res, $Val extends NavigationItem> + implements $NavigationItemCopyWith<$Res> { + _$NavigationItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? icon = null, + Object? label = null, + Object? description = freezed, + Object? fragment = null, + Object? path = freezed, + Object? modes = null, + }) { + return _then(_value.copyWith( + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as Icon, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + fragment: null == fragment + ? _value.fragment + : fragment // ignore: cast_nullable_to_non_nullable + as Widget, + path: freezed == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String?, + modes: null == modes + ? _value.modes + : modes // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NavigationItemImplCopyWith<$Res> + implements $NavigationItemCopyWith<$Res> { + factory _$$NavigationItemImplCopyWith(_$NavigationItemImpl value, + $Res Function(_$NavigationItemImpl) then) = + __$$NavigationItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Icon icon, + String label, + String? description, + Widget fragment, + String? path, + List modes}); +} + +/// @nodoc +class __$$NavigationItemImplCopyWithImpl<$Res> + extends _$NavigationItemCopyWithImpl<$Res, _$NavigationItemImpl> + implements _$$NavigationItemImplCopyWith<$Res> { + __$$NavigationItemImplCopyWithImpl( + _$NavigationItemImpl _value, $Res Function(_$NavigationItemImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? icon = null, + Object? label = null, + Object? description = freezed, + Object? fragment = null, + Object? path = freezed, + Object? modes = null, + }) { + return _then(_$NavigationItemImpl( + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as Icon, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + fragment: null == fragment + ? _value.fragment + : fragment // ignore: cast_nullable_to_non_nullable + as Widget, + path: freezed == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String?, + modes: null == modes + ? _value._modes + : modes // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$NavigationItemImpl implements _NavigationItem { + const _$NavigationItemImpl( + {required this.icon, + required this.label, + this.description, + required this.fragment, + this.path, + final List modes = const [ + NavigationItemMode.mobile, + NavigationItemMode.desktop + ]}) + : _modes = modes; + + @override + final Icon icon; + @override + final String label; + @override + final String? description; + @override + final Widget fragment; + @override + final String? path; + final List _modes; + @override + @JsonKey() + List get modes { + if (_modes is EqualUnmodifiableListView) return _modes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modes); + } + + @override + String toString() { + return 'NavigationItem(icon: $icon, label: $label, description: $description, fragment: $fragment, path: $path, modes: $modes)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NavigationItemImpl && + (identical(other.icon, icon) || other.icon == icon) && + (identical(other.label, label) || other.label == label) && + (identical(other.description, description) || + other.description == description) && + (identical(other.fragment, fragment) || + other.fragment == fragment) && + (identical(other.path, path) || other.path == path) && + const DeepCollectionEquality().equals(other._modes, _modes)); + } + + @override + int get hashCode => Object.hash(runtimeType, icon, label, description, + fragment, path, const DeepCollectionEquality().hash(_modes)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NavigationItemImplCopyWith<_$NavigationItemImpl> get copyWith => + __$$NavigationItemImplCopyWithImpl<_$NavigationItemImpl>( + this, _$identity); +} + +abstract class _NavigationItem implements NavigationItem { + const factory _NavigationItem( + {required final Icon icon, + required final String label, + final String? description, + required final Widget fragment, + final String? path, + final List modes}) = _$NavigationItemImpl; + + @override + Icon get icon; + @override + String get label; + @override + String? get description; + @override + Widget get fragment; + @override + String? get path; + @override + List get modes; + @override + @JsonKey(ignore: true) + _$$NavigationItemImplCopyWith<_$NavigationItemImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/package.freezed.dart b/lib/models/generated/package.freezed.dart new file mode 100644 index 0000000..8109b08 --- /dev/null +++ b/lib/models/generated/package.freezed.dart @@ -0,0 +1,185 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../package.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Package _$PackageFromJson(Map json) { + return _Package.fromJson(json); +} + +/// @nodoc +mixin _$Package { + String get packageName => throw _privateConstructorUsedError; + String get label => throw _privateConstructorUsedError; + bool get isSystem => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PackageCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PackageCopyWith<$Res> { + factory $PackageCopyWith(Package value, $Res Function(Package) then) = + _$PackageCopyWithImpl<$Res, Package>; + @useResult + $Res call({String packageName, String label, bool isSystem}); +} + +/// @nodoc +class _$PackageCopyWithImpl<$Res, $Val extends Package> + implements $PackageCopyWith<$Res> { + _$PackageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? packageName = null, + Object? label = null, + Object? isSystem = null, + }) { + return _then(_value.copyWith( + packageName: null == packageName + ? _value.packageName + : packageName // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + isSystem: null == isSystem + ? _value.isSystem + : isSystem // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> { + factory _$$PackageImplCopyWith( + _$PackageImpl value, $Res Function(_$PackageImpl) then) = + __$$PackageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String packageName, String label, bool isSystem}); +} + +/// @nodoc +class __$$PackageImplCopyWithImpl<$Res> + extends _$PackageCopyWithImpl<$Res, _$PackageImpl> + implements _$$PackageImplCopyWith<$Res> { + __$$PackageImplCopyWithImpl( + _$PackageImpl _value, $Res Function(_$PackageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? packageName = null, + Object? label = null, + Object? isSystem = null, + }) { + return _then(_$PackageImpl( + packageName: null == packageName + ? _value.packageName + : packageName // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + isSystem: null == isSystem + ? _value.isSystem + : isSystem // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PackageImpl implements _Package { + const _$PackageImpl( + {required this.packageName, required this.label, required this.isSystem}); + + factory _$PackageImpl.fromJson(Map json) => + _$$PackageImplFromJson(json); + + @override + final String packageName; + @override + final String label; + @override + final bool isSystem; + + @override + String toString() { + return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PackageImpl && + (identical(other.packageName, packageName) || + other.packageName == packageName) && + (identical(other.label, label) || other.label == label) && + (identical(other.isSystem, isSystem) || + other.isSystem == isSystem)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, packageName, label, isSystem); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PackageImplCopyWith<_$PackageImpl> get copyWith => + __$$PackageImplCopyWithImpl<_$PackageImpl>(this, _$identity); + + @override + Map toJson() { + return _$$PackageImplToJson( + this, + ); + } +} + +abstract class _Package implements Package { + const factory _Package( + {required final String packageName, + required final String label, + required final bool isSystem}) = _$PackageImpl; + + factory _Package.fromJson(Map json) = _$PackageImpl.fromJson; + + @override + String get packageName; + @override + String get label; + @override + bool get isSystem; + @override + @JsonKey(ignore: true) + _$$PackageImplCopyWith<_$PackageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/package.g.dart b/lib/models/generated/package.g.dart new file mode 100644 index 0000000..97b539c --- /dev/null +++ b/lib/models/generated/package.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../package.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PackageImpl _$$PackageImplFromJson(Map json) => + _$PackageImpl( + packageName: json['packageName'] as String, + label: json['label'] as String, + isSystem: json['isSystem'] as bool, + ); + +Map _$$PackageImplToJson(_$PackageImpl instance) => + { + 'packageName': instance.packageName, + 'label': instance.label, + 'isSystem': instance.isSystem, + }; diff --git a/lib/models/generated/profile.g.dart b/lib/models/generated/profile.g.dart new file mode 100644 index 0000000..1be65b2 --- /dev/null +++ b/lib/models/generated/profile.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../profile.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserInfo _$UserInfoFromJson(Map json) => UserInfo( + upload: (json['upload'] as num?)?.toInt(), + download: (json['download'] as num?)?.toInt(), + total: (json['total'] as num?)?.toInt(), + expire: (json['expire'] as num?)?.toInt(), + ); + +Map _$UserInfoToJson(UserInfo instance) => { + 'upload': instance.upload, + 'download': instance.download, + 'total': instance.total, + 'expire': instance.expire, + }; + +Profile _$ProfileFromJson(Map json) => Profile( + id: json['id'] as String?, + label: json['label'] as String?, + url: json['url'] as String?, + userInfo: json['userInfo'] == null + ? null + : UserInfo.fromJson(json['userInfo'] as Map), + groupName: json['groupName'] as String?, + proxyName: json['proxyName'] as String?, + lastUpdateDate: json['lastUpdateDate'] == null + ? null + : DateTime.parse(json['lastUpdateDate'] as String), + autoUpdateDuration: json['autoUpdateDuration'] == null + ? null + : Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()), + autoUpdate: json['autoUpdate'] as bool? ?? true, + ); + +Map _$ProfileToJson(Profile instance) => { + 'id': instance.id, + 'label': instance.label, + 'groupName': instance.groupName, + 'proxyName': instance.proxyName, + 'url': instance.url, + 'lastUpdateDate': instance.lastUpdateDate?.toIso8601String(), + 'autoUpdateDuration': instance.autoUpdateDuration.inMicroseconds, + 'userInfo': instance.userInfo, + 'autoUpdate': instance.autoUpdate, + }; diff --git a/lib/models/generated/proxy.freezed.dart b/lib/models/generated/proxy.freezed.dart new file mode 100644 index 0000000..fd40361 --- /dev/null +++ b/lib/models/generated/proxy.freezed.dart @@ -0,0 +1,362 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../proxy.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Group _$GroupFromJson(Map json) { + return _Group.fromJson(json); +} + +/// @nodoc +mixin _$Group { + GroupType get type => throw _privateConstructorUsedError; + List get all => throw _privateConstructorUsedError; + String? get now => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $GroupCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GroupCopyWith<$Res> { + factory $GroupCopyWith(Group value, $Res Function(Group) then) = + _$GroupCopyWithImpl<$Res, Group>; + @useResult + $Res call({GroupType type, List all, String? now, String name}); +} + +/// @nodoc +class _$GroupCopyWithImpl<$Res, $Val extends Group> + implements $GroupCopyWith<$Res> { + _$GroupCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? all = null, + Object? now = freezed, + Object? name = null, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as GroupType, + all: null == all + ? _value.all + : all // ignore: cast_nullable_to_non_nullable + as List, + now: freezed == now + ? _value.now + : now // ignore: cast_nullable_to_non_nullable + as String?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> { + factory _$$GroupImplCopyWith( + _$GroupImpl value, $Res Function(_$GroupImpl) then) = + __$$GroupImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({GroupType type, List all, String? now, String name}); +} + +/// @nodoc +class __$$GroupImplCopyWithImpl<$Res> + extends _$GroupCopyWithImpl<$Res, _$GroupImpl> + implements _$$GroupImplCopyWith<$Res> { + __$$GroupImplCopyWithImpl( + _$GroupImpl _value, $Res Function(_$GroupImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? all = null, + Object? now = freezed, + Object? name = null, + }) { + return _then(_$GroupImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as GroupType, + all: null == all + ? _value._all + : all // ignore: cast_nullable_to_non_nullable + as List, + now: freezed == now + ? _value.now + : now // ignore: cast_nullable_to_non_nullable + as String?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$GroupImpl implements _Group { + const _$GroupImpl( + {required this.type, + final List all = const [], + this.now, + required this.name}) + : _all = all; + + factory _$GroupImpl.fromJson(Map json) => + _$$GroupImplFromJson(json); + + @override + final GroupType type; + final List _all; + @override + @JsonKey() + List get all { + if (_all is EqualUnmodifiableListView) return _all; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_all); + } + + @override + final String? now; + @override + final String name; + + @override + String toString() { + return 'Group(type: $type, all: $all, now: $now, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GroupImpl && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other._all, _all) && + (identical(other.now, now) || other.now == now) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, type, const DeepCollectionEquality().hash(_all), now, name); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$GroupImplCopyWith<_$GroupImpl> get copyWith => + __$$GroupImplCopyWithImpl<_$GroupImpl>(this, _$identity); + + @override + Map toJson() { + return _$$GroupImplToJson( + this, + ); + } +} + +abstract class _Group implements Group { + const factory _Group( + {required final GroupType type, + final List all, + final String? now, + required final String name}) = _$GroupImpl; + + factory _Group.fromJson(Map json) = _$GroupImpl.fromJson; + + @override + GroupType get type; + @override + List get all; + @override + String? get now; + @override + String get name; + @override + @JsonKey(ignore: true) + _$$GroupImplCopyWith<_$GroupImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Proxy _$ProxyFromJson(Map json) { + return _Proxy.fromJson(json); +} + +/// @nodoc +mixin _$Proxy { + String get name => throw _privateConstructorUsedError; + String get type => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ProxyCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProxyCopyWith<$Res> { + factory $ProxyCopyWith(Proxy value, $Res Function(Proxy) then) = + _$ProxyCopyWithImpl<$Res, Proxy>; + @useResult + $Res call({String name, String type}); +} + +/// @nodoc +class _$ProxyCopyWithImpl<$Res, $Val extends Proxy> + implements $ProxyCopyWith<$Res> { + _$ProxyCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? type = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProxyImplCopyWith<$Res> implements $ProxyCopyWith<$Res> { + factory _$$ProxyImplCopyWith( + _$ProxyImpl value, $Res Function(_$ProxyImpl) then) = + __$$ProxyImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String type}); +} + +/// @nodoc +class __$$ProxyImplCopyWithImpl<$Res> + extends _$ProxyCopyWithImpl<$Res, _$ProxyImpl> + implements _$$ProxyImplCopyWith<$Res> { + __$$ProxyImplCopyWithImpl( + _$ProxyImpl _value, $Res Function(_$ProxyImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? type = null, + }) { + return _then(_$ProxyImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ProxyImpl implements _Proxy { + const _$ProxyImpl({this.name = "", this.type = ""}); + + factory _$ProxyImpl.fromJson(Map json) => + _$$ProxyImplFromJson(json); + + @override + @JsonKey() + final String name; + @override + @JsonKey() + final String type; + + @override + String toString() { + return 'Proxy(name: $name, type: $type)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProxyImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.type, type) || other.type == type)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, type); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProxyImplCopyWith<_$ProxyImpl> get copyWith => + __$$ProxyImplCopyWithImpl<_$ProxyImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ProxyImplToJson( + this, + ); + } +} + +abstract class _Proxy implements Proxy { + const factory _Proxy({final String name, final String type}) = _$ProxyImpl; + + factory _Proxy.fromJson(Map json) = _$ProxyImpl.fromJson; + + @override + String get name; + @override + String get type; + @override + @JsonKey(ignore: true) + _$$ProxyImplCopyWith<_$ProxyImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/proxy.g.dart b/lib/models/generated/proxy.g.dart new file mode 100644 index 0000000..30ea819 --- /dev/null +++ b/lib/models/generated/proxy.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../proxy.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GroupImpl _$$GroupImplFromJson(Map json) => _$GroupImpl( + type: $enumDecode(_$GroupTypeEnumMap, json['type']), + all: (json['all'] as List?) + ?.map((e) => Proxy.fromJson(e as Map)) + .toList() ?? + const [], + now: json['now'] as String?, + name: json['name'] as String, + ); + +Map _$$GroupImplToJson(_$GroupImpl instance) => + { + 'type': _$GroupTypeEnumMap[instance.type]!, + 'all': instance.all, + 'now': instance.now, + 'name': instance.name, + }; + +const _$GroupTypeEnumMap = { + GroupType.Selector: 'Selector', + GroupType.URLTest: 'URLTest', + GroupType.Fallback: 'Fallback', +}; + +_$ProxyImpl _$$ProxyImplFromJson(Map json) => _$ProxyImpl( + name: json['name'] as String? ?? "", + type: json['type'] as String? ?? "", + ); + +Map _$$ProxyImplToJson(_$ProxyImpl instance) => + { + 'name': instance.name, + 'type': instance.type, + }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart new file mode 100644 index 0000000..3a747d5 --- /dev/null +++ b/lib/models/generated/selector.freezed.dart @@ -0,0 +1,1742 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../selector.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$StartButtonSelectorState { + bool get isInit => throw _privateConstructorUsedError; + bool get hasProfile => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $StartButtonSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $StartButtonSelectorStateCopyWith<$Res> { + factory $StartButtonSelectorStateCopyWith(StartButtonSelectorState value, + $Res Function(StartButtonSelectorState) then) = + _$StartButtonSelectorStateCopyWithImpl<$Res, StartButtonSelectorState>; + @useResult + $Res call({bool isInit, bool hasProfile}); +} + +/// @nodoc +class _$StartButtonSelectorStateCopyWithImpl<$Res, + $Val extends StartButtonSelectorState> + implements $StartButtonSelectorStateCopyWith<$Res> { + _$StartButtonSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInit = null, + Object? hasProfile = null, + }) { + return _then(_value.copyWith( + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + hasProfile: null == hasProfile + ? _value.hasProfile + : hasProfile // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$StartButtonSelectorStateImplCopyWith<$Res> + implements $StartButtonSelectorStateCopyWith<$Res> { + factory _$$StartButtonSelectorStateImplCopyWith( + _$StartButtonSelectorStateImpl value, + $Res Function(_$StartButtonSelectorStateImpl) then) = + __$$StartButtonSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isInit, bool hasProfile}); +} + +/// @nodoc +class __$$StartButtonSelectorStateImplCopyWithImpl<$Res> + extends _$StartButtonSelectorStateCopyWithImpl<$Res, + _$StartButtonSelectorStateImpl> + implements _$$StartButtonSelectorStateImplCopyWith<$Res> { + __$$StartButtonSelectorStateImplCopyWithImpl( + _$StartButtonSelectorStateImpl _value, + $Res Function(_$StartButtonSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInit = null, + Object? hasProfile = null, + }) { + return _then(_$StartButtonSelectorStateImpl( + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + hasProfile: null == hasProfile + ? _value.hasProfile + : hasProfile // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$StartButtonSelectorStateImpl implements _StartButtonSelectorState { + const _$StartButtonSelectorStateImpl( + {required this.isInit, required this.hasProfile}); + + @override + final bool isInit; + @override + final bool hasProfile; + + @override + String toString() { + return 'StartButtonSelectorState(isInit: $isInit, hasProfile: $hasProfile)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$StartButtonSelectorStateImpl && + (identical(other.isInit, isInit) || other.isInit == isInit) && + (identical(other.hasProfile, hasProfile) || + other.hasProfile == hasProfile)); + } + + @override + int get hashCode => Object.hash(runtimeType, isInit, hasProfile); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$StartButtonSelectorStateImplCopyWith<_$StartButtonSelectorStateImpl> + get copyWith => __$$StartButtonSelectorStateImplCopyWithImpl< + _$StartButtonSelectorStateImpl>(this, _$identity); +} + +abstract class _StartButtonSelectorState implements StartButtonSelectorState { + const factory _StartButtonSelectorState( + {required final bool isInit, + required final bool hasProfile}) = _$StartButtonSelectorStateImpl; + + @override + bool get isInit; + @override + bool get hasProfile; + @override + @JsonKey(ignore: true) + _$$StartButtonSelectorStateImplCopyWith<_$StartButtonSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$UpdateCurrentDelaySelectorState { + String? get currentProxyName => throw _privateConstructorUsedError; + bool get isCurrent => throw _privateConstructorUsedError; + int? get delay => throw _privateConstructorUsedError; + bool get isInit => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $UpdateCurrentDelaySelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateCurrentDelaySelectorStateCopyWith<$Res> { + factory $UpdateCurrentDelaySelectorStateCopyWith( + UpdateCurrentDelaySelectorState value, + $Res Function(UpdateCurrentDelaySelectorState) then) = + _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res, + UpdateCurrentDelaySelectorState>; + @useResult + $Res call( + {String? currentProxyName, bool isCurrent, int? delay, bool isInit}); +} + +/// @nodoc +class _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res, + $Val extends UpdateCurrentDelaySelectorState> + implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> { + _$UpdateCurrentDelaySelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentProxyName = freezed, + Object? isCurrent = null, + Object? delay = freezed, + Object? isInit = null, + }) { + return _then(_value.copyWith( + currentProxyName: freezed == currentProxyName + ? _value.currentProxyName + : currentProxyName // ignore: cast_nullable_to_non_nullable + as String?, + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + delay: freezed == delay + ? _value.delay + : delay // ignore: cast_nullable_to_non_nullable + as int?, + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res> + implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> { + factory _$$UpdateCurrentDelaySelectorStateImplCopyWith( + _$UpdateCurrentDelaySelectorStateImpl value, + $Res Function(_$UpdateCurrentDelaySelectorStateImpl) then) = + __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? currentProxyName, bool isCurrent, int? delay, bool isInit}); +} + +/// @nodoc +class __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res> + extends _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res, + _$UpdateCurrentDelaySelectorStateImpl> + implements _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res> { + __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl( + _$UpdateCurrentDelaySelectorStateImpl _value, + $Res Function(_$UpdateCurrentDelaySelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentProxyName = freezed, + Object? isCurrent = null, + Object? delay = freezed, + Object? isInit = null, + }) { + return _then(_$UpdateCurrentDelaySelectorStateImpl( + currentProxyName: freezed == currentProxyName + ? _value.currentProxyName + : currentProxyName // ignore: cast_nullable_to_non_nullable + as String?, + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + delay: freezed == delay + ? _value.delay + : delay // ignore: cast_nullable_to_non_nullable + as int?, + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$UpdateCurrentDelaySelectorStateImpl + implements _UpdateCurrentDelaySelectorState { + const _$UpdateCurrentDelaySelectorStateImpl( + {required this.currentProxyName, + required this.isCurrent, + required this.delay, + required this.isInit}); + + @override + final String? currentProxyName; + @override + final bool isCurrent; + @override + final int? delay; + @override + final bool isInit; + + @override + String toString() { + return 'UpdateCurrentDelaySelectorState(currentProxyName: $currentProxyName, isCurrent: $isCurrent, delay: $delay, isInit: $isInit)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateCurrentDelaySelectorStateImpl && + (identical(other.currentProxyName, currentProxyName) || + other.currentProxyName == currentProxyName) && + (identical(other.isCurrent, isCurrent) || + other.isCurrent == isCurrent) && + (identical(other.delay, delay) || other.delay == delay) && + (identical(other.isInit, isInit) || other.isInit == isInit)); + } + + @override + int get hashCode => + Object.hash(runtimeType, currentProxyName, isCurrent, delay, isInit); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UpdateCurrentDelaySelectorStateImplCopyWith< + _$UpdateCurrentDelaySelectorStateImpl> + get copyWith => __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl< + _$UpdateCurrentDelaySelectorStateImpl>(this, _$identity); +} + +abstract class _UpdateCurrentDelaySelectorState + implements UpdateCurrentDelaySelectorState { + const factory _UpdateCurrentDelaySelectorState( + {required final String? currentProxyName, + required final bool isCurrent, + required final int? delay, + required final bool isInit}) = _$UpdateCurrentDelaySelectorStateImpl; + + @override + String? get currentProxyName; + @override + bool get isCurrent; + @override + int? get delay; + @override + bool get isInit; + @override + @JsonKey(ignore: true) + _$$UpdateCurrentDelaySelectorStateImplCopyWith< + _$UpdateCurrentDelaySelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$NetworkDetectionSelectorState { + String? get currentProxyName => throw _privateConstructorUsedError; + int? get delay => throw _privateConstructorUsedError; + bool get isInit => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $NetworkDetectionSelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NetworkDetectionSelectorStateCopyWith<$Res> { + factory $NetworkDetectionSelectorStateCopyWith( + NetworkDetectionSelectorState value, + $Res Function(NetworkDetectionSelectorState) then) = + _$NetworkDetectionSelectorStateCopyWithImpl<$Res, + NetworkDetectionSelectorState>; + @useResult + $Res call({String? currentProxyName, int? delay, bool isInit}); +} + +/// @nodoc +class _$NetworkDetectionSelectorStateCopyWithImpl<$Res, + $Val extends NetworkDetectionSelectorState> + implements $NetworkDetectionSelectorStateCopyWith<$Res> { + _$NetworkDetectionSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentProxyName = freezed, + Object? delay = freezed, + Object? isInit = null, + }) { + return _then(_value.copyWith( + currentProxyName: freezed == currentProxyName + ? _value.currentProxyName + : currentProxyName // ignore: cast_nullable_to_non_nullable + as String?, + delay: freezed == delay + ? _value.delay + : delay // ignore: cast_nullable_to_non_nullable + as int?, + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NetworkDetectionSelectorStateImplCopyWith<$Res> + implements $NetworkDetectionSelectorStateCopyWith<$Res> { + factory _$$NetworkDetectionSelectorStateImplCopyWith( + _$NetworkDetectionSelectorStateImpl value, + $Res Function(_$NetworkDetectionSelectorStateImpl) then) = + __$$NetworkDetectionSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? currentProxyName, int? delay, bool isInit}); +} + +/// @nodoc +class __$$NetworkDetectionSelectorStateImplCopyWithImpl<$Res> + extends _$NetworkDetectionSelectorStateCopyWithImpl<$Res, + _$NetworkDetectionSelectorStateImpl> + implements _$$NetworkDetectionSelectorStateImplCopyWith<$Res> { + __$$NetworkDetectionSelectorStateImplCopyWithImpl( + _$NetworkDetectionSelectorStateImpl _value, + $Res Function(_$NetworkDetectionSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentProxyName = freezed, + Object? delay = freezed, + Object? isInit = null, + }) { + return _then(_$NetworkDetectionSelectorStateImpl( + currentProxyName: freezed == currentProxyName + ? _value.currentProxyName + : currentProxyName // ignore: cast_nullable_to_non_nullable + as String?, + delay: freezed == delay + ? _value.delay + : delay // ignore: cast_nullable_to_non_nullable + as int?, + isInit: null == isInit + ? _value.isInit + : isInit // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$NetworkDetectionSelectorStateImpl + implements _NetworkDetectionSelectorState { + const _$NetworkDetectionSelectorStateImpl( + {required this.currentProxyName, + required this.delay, + required this.isInit}); + + @override + final String? currentProxyName; + @override + final int? delay; + @override + final bool isInit; + + @override + String toString() { + return 'NetworkDetectionSelectorState(currentProxyName: $currentProxyName, delay: $delay, isInit: $isInit)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NetworkDetectionSelectorStateImpl && + (identical(other.currentProxyName, currentProxyName) || + other.currentProxyName == currentProxyName) && + (identical(other.delay, delay) || other.delay == delay) && + (identical(other.isInit, isInit) || other.isInit == isInit)); + } + + @override + int get hashCode => Object.hash(runtimeType, currentProxyName, delay, isInit); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NetworkDetectionSelectorStateImplCopyWith< + _$NetworkDetectionSelectorStateImpl> + get copyWith => __$$NetworkDetectionSelectorStateImplCopyWithImpl< + _$NetworkDetectionSelectorStateImpl>(this, _$identity); +} + +abstract class _NetworkDetectionSelectorState + implements NetworkDetectionSelectorState { + const factory _NetworkDetectionSelectorState( + {required final String? currentProxyName, + required final int? delay, + required final bool isInit}) = _$NetworkDetectionSelectorStateImpl; + + @override + String? get currentProxyName; + @override + int? get delay; + @override + bool get isInit; + @override + @JsonKey(ignore: true) + _$$NetworkDetectionSelectorStateImplCopyWith< + _$NetworkDetectionSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ProfilesSelectorState { + List get profiles => throw _privateConstructorUsedError; + String? get currentProfileId => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ProfilesSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfilesSelectorStateCopyWith<$Res> { + factory $ProfilesSelectorStateCopyWith(ProfilesSelectorState value, + $Res Function(ProfilesSelectorState) then) = + _$ProfilesSelectorStateCopyWithImpl<$Res, ProfilesSelectorState>; + @useResult + $Res call({List profiles, String? currentProfileId}); +} + +/// @nodoc +class _$ProfilesSelectorStateCopyWithImpl<$Res, + $Val extends ProfilesSelectorState> + implements $ProfilesSelectorStateCopyWith<$Res> { + _$ProfilesSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profiles = null, + Object? currentProfileId = freezed, + }) { + return _then(_value.copyWith( + profiles: null == profiles + ? _value.profiles + : profiles // ignore: cast_nullable_to_non_nullable + as List, + currentProfileId: freezed == currentProfileId + ? _value.currentProfileId + : currentProfileId // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProfilesSelectorStateImplCopyWith<$Res> + implements $ProfilesSelectorStateCopyWith<$Res> { + factory _$$ProfilesSelectorStateImplCopyWith( + _$ProfilesSelectorStateImpl value, + $Res Function(_$ProfilesSelectorStateImpl) then) = + __$$ProfilesSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List profiles, String? currentProfileId}); +} + +/// @nodoc +class __$$ProfilesSelectorStateImplCopyWithImpl<$Res> + extends _$ProfilesSelectorStateCopyWithImpl<$Res, + _$ProfilesSelectorStateImpl> + implements _$$ProfilesSelectorStateImplCopyWith<$Res> { + __$$ProfilesSelectorStateImplCopyWithImpl(_$ProfilesSelectorStateImpl _value, + $Res Function(_$ProfilesSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? profiles = null, + Object? currentProfileId = freezed, + }) { + return _then(_$ProfilesSelectorStateImpl( + profiles: null == profiles + ? _value._profiles + : profiles // ignore: cast_nullable_to_non_nullable + as List, + currentProfileId: freezed == currentProfileId + ? _value.currentProfileId + : currentProfileId // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState { + const _$ProfilesSelectorStateImpl( + {required final List profiles, required this.currentProfileId}) + : _profiles = profiles; + + final List _profiles; + @override + List get profiles { + if (_profiles is EqualUnmodifiableListView) return _profiles; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_profiles); + } + + @override + final String? currentProfileId; + + @override + String toString() { + return 'ProfilesSelectorState(profiles: $profiles, currentProfileId: $currentProfileId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProfilesSelectorStateImpl && + const DeepCollectionEquality().equals(other._profiles, _profiles) && + (identical(other.currentProfileId, currentProfileId) || + other.currentProfileId == currentProfileId)); + } + + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_profiles), currentProfileId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProfilesSelectorStateImplCopyWith<_$ProfilesSelectorStateImpl> + get copyWith => __$$ProfilesSelectorStateImplCopyWithImpl< + _$ProfilesSelectorStateImpl>(this, _$identity); +} + +abstract class _ProfilesSelectorState implements ProfilesSelectorState { + const factory _ProfilesSelectorState( + {required final List profiles, + required final String? currentProfileId}) = _$ProfilesSelectorStateImpl; + + @override + List get profiles; + @override + String? get currentProfileId; + @override + @JsonKey(ignore: true) + _$$ProfilesSelectorStateImplCopyWith<_$ProfilesSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PackageListSelectorState { + AccessControl get accessControl => throw _privateConstructorUsedError; + List get packages => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PackageListSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PackageListSelectorStateCopyWith<$Res> { + factory $PackageListSelectorStateCopyWith(PackageListSelectorState value, + $Res Function(PackageListSelectorState) then) = + _$PackageListSelectorStateCopyWithImpl<$Res, PackageListSelectorState>; + @useResult + $Res call({AccessControl accessControl, List packages}); +} + +/// @nodoc +class _$PackageListSelectorStateCopyWithImpl<$Res, + $Val extends PackageListSelectorState> + implements $PackageListSelectorStateCopyWith<$Res> { + _$PackageListSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessControl = null, + Object? packages = null, + }) { + return _then(_value.copyWith( + accessControl: null == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl, + packages: null == packages + ? _value.packages + : packages // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PackageListSelectorStateImplCopyWith<$Res> + implements $PackageListSelectorStateCopyWith<$Res> { + factory _$$PackageListSelectorStateImplCopyWith( + _$PackageListSelectorStateImpl value, + $Res Function(_$PackageListSelectorStateImpl) then) = + __$$PackageListSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({AccessControl accessControl, List packages}); +} + +/// @nodoc +class __$$PackageListSelectorStateImplCopyWithImpl<$Res> + extends _$PackageListSelectorStateCopyWithImpl<$Res, + _$PackageListSelectorStateImpl> + implements _$$PackageListSelectorStateImplCopyWith<$Res> { + __$$PackageListSelectorStateImplCopyWithImpl( + _$PackageListSelectorStateImpl _value, + $Res Function(_$PackageListSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessControl = null, + Object? packages = null, + }) { + return _then(_$PackageListSelectorStateImpl( + accessControl: null == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl, + packages: null == packages + ? _value._packages + : packages // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$PackageListSelectorStateImpl implements _PackageListSelectorState { + const _$PackageListSelectorStateImpl( + {required this.accessControl, required final List packages}) + : _packages = packages; + + @override + final AccessControl accessControl; + final List _packages; + @override + List get packages { + if (_packages is EqualUnmodifiableListView) return _packages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_packages); + } + + @override + String toString() { + return 'PackageListSelectorState(accessControl: $accessControl, packages: $packages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PackageListSelectorStateImpl && + (identical(other.accessControl, accessControl) || + other.accessControl == accessControl) && + const DeepCollectionEquality().equals(other._packages, _packages)); + } + + @override + int get hashCode => Object.hash(runtimeType, accessControl, + const DeepCollectionEquality().hash(_packages)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl> + get copyWith => __$$PackageListSelectorStateImplCopyWithImpl< + _$PackageListSelectorStateImpl>(this, _$identity); +} + +abstract class _PackageListSelectorState implements PackageListSelectorState { + const factory _PackageListSelectorState( + {required final AccessControl accessControl, + required final List packages}) = _$PackageListSelectorStateImpl; + + @override + AccessControl get accessControl; + @override + List get packages; + @override + @JsonKey(ignore: true) + _$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ApplicationSelectorState { + String? get locale => throw _privateConstructorUsedError; + ThemeMode? get themeMode => throw _privateConstructorUsedError; + int? get primaryColor => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ApplicationSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ApplicationSelectorStateCopyWith<$Res> { + factory $ApplicationSelectorStateCopyWith(ApplicationSelectorState value, + $Res Function(ApplicationSelectorState) then) = + _$ApplicationSelectorStateCopyWithImpl<$Res, ApplicationSelectorState>; + @useResult + $Res call({String? locale, ThemeMode? themeMode, int? primaryColor}); +} + +/// @nodoc +class _$ApplicationSelectorStateCopyWithImpl<$Res, + $Val extends ApplicationSelectorState> + implements $ApplicationSelectorStateCopyWith<$Res> { + _$ApplicationSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? locale = freezed, + Object? themeMode = freezed, + Object? primaryColor = freezed, + }) { + return _then(_value.copyWith( + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + themeMode: freezed == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode?, + primaryColor: freezed == primaryColor + ? _value.primaryColor + : primaryColor // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ApplicationSelectorStateImplCopyWith<$Res> + implements $ApplicationSelectorStateCopyWith<$Res> { + factory _$$ApplicationSelectorStateImplCopyWith( + _$ApplicationSelectorStateImpl value, + $Res Function(_$ApplicationSelectorStateImpl) then) = + __$$ApplicationSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? locale, ThemeMode? themeMode, int? primaryColor}); +} + +/// @nodoc +class __$$ApplicationSelectorStateImplCopyWithImpl<$Res> + extends _$ApplicationSelectorStateCopyWithImpl<$Res, + _$ApplicationSelectorStateImpl> + implements _$$ApplicationSelectorStateImplCopyWith<$Res> { + __$$ApplicationSelectorStateImplCopyWithImpl( + _$ApplicationSelectorStateImpl _value, + $Res Function(_$ApplicationSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? locale = freezed, + Object? themeMode = freezed, + Object? primaryColor = freezed, + }) { + return _then(_$ApplicationSelectorStateImpl( + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + themeMode: freezed == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode?, + primaryColor: freezed == primaryColor + ? _value.primaryColor + : primaryColor // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc + +class _$ApplicationSelectorStateImpl implements _ApplicationSelectorState { + const _$ApplicationSelectorStateImpl( + {this.locale, this.themeMode, this.primaryColor}); + + @override + final String? locale; + @override + final ThemeMode? themeMode; + @override + final int? primaryColor; + + @override + String toString() { + return 'ApplicationSelectorState(locale: $locale, themeMode: $themeMode, primaryColor: $primaryColor)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ApplicationSelectorStateImpl && + (identical(other.locale, locale) || other.locale == locale) && + (identical(other.themeMode, themeMode) || + other.themeMode == themeMode) && + (identical(other.primaryColor, primaryColor) || + other.primaryColor == primaryColor)); + } + + @override + int get hashCode => Object.hash(runtimeType, locale, themeMode, primaryColor); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ApplicationSelectorStateImplCopyWith<_$ApplicationSelectorStateImpl> + get copyWith => __$$ApplicationSelectorStateImplCopyWithImpl< + _$ApplicationSelectorStateImpl>(this, _$identity); +} + +abstract class _ApplicationSelectorState implements ApplicationSelectorState { + const factory _ApplicationSelectorState( + {final String? locale, + final ThemeMode? themeMode, + final int? primaryColor}) = _$ApplicationSelectorStateImpl; + + @override + String? get locale; + @override + ThemeMode? get themeMode; + @override + int? get primaryColor; + @override + @JsonKey(ignore: true) + _$$ApplicationSelectorStateImplCopyWith<_$ApplicationSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$HomeLayoutSelectorState { + List get navigationItems => + throw _privateConstructorUsedError; + int get currentIndex => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeLayoutSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeLayoutSelectorStateCopyWith<$Res> { + factory $HomeLayoutSelectorStateCopyWith(HomeLayoutSelectorState value, + $Res Function(HomeLayoutSelectorState) then) = + _$HomeLayoutSelectorStateCopyWithImpl<$Res, HomeLayoutSelectorState>; + @useResult + $Res call({List navigationItems, int currentIndex}); +} + +/// @nodoc +class _$HomeLayoutSelectorStateCopyWithImpl<$Res, + $Val extends HomeLayoutSelectorState> + implements $HomeLayoutSelectorStateCopyWith<$Res> { + _$HomeLayoutSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? navigationItems = null, + Object? currentIndex = null, + }) { + return _then(_value.copyWith( + navigationItems: null == navigationItems + ? _value.navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeLayoutSelectorStateImplCopyWith<$Res> + implements $HomeLayoutSelectorStateCopyWith<$Res> { + factory _$$HomeLayoutSelectorStateImplCopyWith( + _$HomeLayoutSelectorStateImpl value, + $Res Function(_$HomeLayoutSelectorStateImpl) then) = + __$$HomeLayoutSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List navigationItems, int currentIndex}); +} + +/// @nodoc +class __$$HomeLayoutSelectorStateImplCopyWithImpl<$Res> + extends _$HomeLayoutSelectorStateCopyWithImpl<$Res, + _$HomeLayoutSelectorStateImpl> + implements _$$HomeLayoutSelectorStateImplCopyWith<$Res> { + __$$HomeLayoutSelectorStateImplCopyWithImpl( + _$HomeLayoutSelectorStateImpl _value, + $Res Function(_$HomeLayoutSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? navigationItems = null, + Object? currentIndex = null, + }) { + return _then(_$HomeLayoutSelectorStateImpl( + navigationItems: null == navigationItems + ? _value._navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$HomeLayoutSelectorStateImpl implements _HomeLayoutSelectorState { + const _$HomeLayoutSelectorStateImpl( + {required final List navigationItems, + required this.currentIndex}) + : _navigationItems = navigationItems; + + final List _navigationItems; + @override + List get navigationItems { + if (_navigationItems is EqualUnmodifiableListView) return _navigationItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_navigationItems); + } + + @override + final int currentIndex; + + @override + String toString() { + return 'HomeLayoutSelectorState(navigationItems: $navigationItems, currentIndex: $currentIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeLayoutSelectorStateImpl && + const DeepCollectionEquality() + .equals(other._navigationItems, _navigationItems) && + (identical(other.currentIndex, currentIndex) || + other.currentIndex == currentIndex)); + } + + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_navigationItems), currentIndex); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeLayoutSelectorStateImplCopyWith<_$HomeLayoutSelectorStateImpl> + get copyWith => __$$HomeLayoutSelectorStateImplCopyWithImpl< + _$HomeLayoutSelectorStateImpl>(this, _$identity); +} + +abstract class _HomeLayoutSelectorState implements HomeLayoutSelectorState { + const factory _HomeLayoutSelectorState( + {required final List navigationItems, + required final int currentIndex}) = _$HomeLayoutSelectorStateImpl; + + @override + List get navigationItems; + @override + int get currentIndex; + @override + @JsonKey(ignore: true) + _$$HomeLayoutSelectorStateImplCopyWith<_$HomeLayoutSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$TrayContainerSelectorState { + Mode get mode => throw _privateConstructorUsedError; + bool get autoLaunch => throw _privateConstructorUsedError; + bool get isRun => throw _privateConstructorUsedError; + String? get locale => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $TrayContainerSelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TrayContainerSelectorStateCopyWith<$Res> { + factory $TrayContainerSelectorStateCopyWith(TrayContainerSelectorState value, + $Res Function(TrayContainerSelectorState) then) = + _$TrayContainerSelectorStateCopyWithImpl<$Res, + TrayContainerSelectorState>; + @useResult + $Res call({Mode mode, bool autoLaunch, bool isRun, String? locale}); +} + +/// @nodoc +class _$TrayContainerSelectorStateCopyWithImpl<$Res, + $Val extends TrayContainerSelectorState> + implements $TrayContainerSelectorStateCopyWith<$Res> { + _$TrayContainerSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? mode = null, + Object? autoLaunch = null, + Object? isRun = null, + Object? locale = freezed, + }) { + return _then(_value.copyWith( + mode: null == mode + ? _value.mode + : mode // ignore: cast_nullable_to_non_nullable + as Mode, + autoLaunch: null == autoLaunch + ? _value.autoLaunch + : autoLaunch // ignore: cast_nullable_to_non_nullable + as bool, + isRun: null == isRun + ? _value.isRun + : isRun // ignore: cast_nullable_to_non_nullable + as bool, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TrayContainerSelectorStateImplCopyWith<$Res> + implements $TrayContainerSelectorStateCopyWith<$Res> { + factory _$$TrayContainerSelectorStateImplCopyWith( + _$TrayContainerSelectorStateImpl value, + $Res Function(_$TrayContainerSelectorStateImpl) then) = + __$$TrayContainerSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Mode mode, bool autoLaunch, bool isRun, String? locale}); +} + +/// @nodoc +class __$$TrayContainerSelectorStateImplCopyWithImpl<$Res> + extends _$TrayContainerSelectorStateCopyWithImpl<$Res, + _$TrayContainerSelectorStateImpl> + implements _$$TrayContainerSelectorStateImplCopyWith<$Res> { + __$$TrayContainerSelectorStateImplCopyWithImpl( + _$TrayContainerSelectorStateImpl _value, + $Res Function(_$TrayContainerSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? mode = null, + Object? autoLaunch = null, + Object? isRun = null, + Object? locale = freezed, + }) { + return _then(_$TrayContainerSelectorStateImpl( + mode: null == mode + ? _value.mode + : mode // ignore: cast_nullable_to_non_nullable + as Mode, + autoLaunch: null == autoLaunch + ? _value.autoLaunch + : autoLaunch // ignore: cast_nullable_to_non_nullable + as bool, + isRun: null == isRun + ? _value.isRun + : isRun // ignore: cast_nullable_to_non_nullable + as bool, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$TrayContainerSelectorStateImpl implements _TrayContainerSelectorState { + const _$TrayContainerSelectorStateImpl( + {required this.mode, + required this.autoLaunch, + required this.isRun, + required this.locale}); + + @override + final Mode mode; + @override + final bool autoLaunch; + @override + final bool isRun; + @override + final String? locale; + + @override + String toString() { + return 'TrayContainerSelectorState(mode: $mode, autoLaunch: $autoLaunch, isRun: $isRun, locale: $locale)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TrayContainerSelectorStateImpl && + (identical(other.mode, mode) || other.mode == mode) && + (identical(other.autoLaunch, autoLaunch) || + other.autoLaunch == autoLaunch) && + (identical(other.isRun, isRun) || other.isRun == isRun) && + (identical(other.locale, locale) || other.locale == locale)); + } + + @override + int get hashCode => Object.hash(runtimeType, mode, autoLaunch, isRun, locale); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TrayContainerSelectorStateImplCopyWith<_$TrayContainerSelectorStateImpl> + get copyWith => __$$TrayContainerSelectorStateImplCopyWithImpl< + _$TrayContainerSelectorStateImpl>(this, _$identity); +} + +abstract class _TrayContainerSelectorState + implements TrayContainerSelectorState { + const factory _TrayContainerSelectorState( + {required final Mode mode, + required final bool autoLaunch, + required final bool isRun, + required final String? locale}) = _$TrayContainerSelectorStateImpl; + + @override + Mode get mode; + @override + bool get autoLaunch; + @override + bool get isRun; + @override + String? get locale; + @override + @JsonKey(ignore: true) + _$$TrayContainerSelectorStateImplCopyWith<_$TrayContainerSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$UpdateNavigationsSelector { + bool get openLogs => throw _privateConstructorUsedError; + bool get hasProxies => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $UpdateNavigationsSelectorCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateNavigationsSelectorCopyWith<$Res> { + factory $UpdateNavigationsSelectorCopyWith(UpdateNavigationsSelector value, + $Res Function(UpdateNavigationsSelector) then) = + _$UpdateNavigationsSelectorCopyWithImpl<$Res, UpdateNavigationsSelector>; + @useResult + $Res call({bool openLogs, bool hasProxies}); +} + +/// @nodoc +class _$UpdateNavigationsSelectorCopyWithImpl<$Res, + $Val extends UpdateNavigationsSelector> + implements $UpdateNavigationsSelectorCopyWith<$Res> { + _$UpdateNavigationsSelectorCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? openLogs = null, + Object? hasProxies = null, + }) { + return _then(_value.copyWith( + openLogs: null == openLogs + ? _value.openLogs + : openLogs // ignore: cast_nullable_to_non_nullable + as bool, + hasProxies: null == hasProxies + ? _value.hasProxies + : hasProxies // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UpdateNavigationsSelectorImplCopyWith<$Res> + implements $UpdateNavigationsSelectorCopyWith<$Res> { + factory _$$UpdateNavigationsSelectorImplCopyWith( + _$UpdateNavigationsSelectorImpl value, + $Res Function(_$UpdateNavigationsSelectorImpl) then) = + __$$UpdateNavigationsSelectorImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool openLogs, bool hasProxies}); +} + +/// @nodoc +class __$$UpdateNavigationsSelectorImplCopyWithImpl<$Res> + extends _$UpdateNavigationsSelectorCopyWithImpl<$Res, + _$UpdateNavigationsSelectorImpl> + implements _$$UpdateNavigationsSelectorImplCopyWith<$Res> { + __$$UpdateNavigationsSelectorImplCopyWithImpl( + _$UpdateNavigationsSelectorImpl _value, + $Res Function(_$UpdateNavigationsSelectorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? openLogs = null, + Object? hasProxies = null, + }) { + return _then(_$UpdateNavigationsSelectorImpl( + openLogs: null == openLogs + ? _value.openLogs + : openLogs // ignore: cast_nullable_to_non_nullable + as bool, + hasProxies: null == hasProxies + ? _value.hasProxies + : hasProxies // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$UpdateNavigationsSelectorImpl implements _UpdateNavigationsSelector { + const _$UpdateNavigationsSelectorImpl( + {required this.openLogs, required this.hasProxies}); + + @override + final bool openLogs; + @override + final bool hasProxies; + + @override + String toString() { + return 'UpdateNavigationsSelector(openLogs: $openLogs, hasProxies: $hasProxies)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateNavigationsSelectorImpl && + (identical(other.openLogs, openLogs) || + other.openLogs == openLogs) && + (identical(other.hasProxies, hasProxies) || + other.hasProxies == hasProxies)); + } + + @override + int get hashCode => Object.hash(runtimeType, openLogs, hasProxies); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UpdateNavigationsSelectorImplCopyWith<_$UpdateNavigationsSelectorImpl> + get copyWith => __$$UpdateNavigationsSelectorImplCopyWithImpl< + _$UpdateNavigationsSelectorImpl>(this, _$identity); +} + +abstract class _UpdateNavigationsSelector implements UpdateNavigationsSelector { + const factory _UpdateNavigationsSelector( + {required final bool openLogs, + required final bool hasProxies}) = _$UpdateNavigationsSelectorImpl; + + @override + bool get openLogs; + @override + bool get hasProxies; + @override + @JsonKey(ignore: true) + _$$UpdateNavigationsSelectorImplCopyWith<_$UpdateNavigationsSelectorImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$HomeCommonScaffoldSelectorState { + String get currentLabel => throw _privateConstructorUsedError; + String? get locale => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeCommonScaffoldSelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeCommonScaffoldSelectorStateCopyWith<$Res> { + factory $HomeCommonScaffoldSelectorStateCopyWith( + HomeCommonScaffoldSelectorState value, + $Res Function(HomeCommonScaffoldSelectorState) then) = + _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, + HomeCommonScaffoldSelectorState>; + @useResult + $Res call({String currentLabel, String? locale}); +} + +/// @nodoc +class _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, + $Val extends HomeCommonScaffoldSelectorState> + implements $HomeCommonScaffoldSelectorStateCopyWith<$Res> { + _$HomeCommonScaffoldSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentLabel = null, + Object? locale = freezed, + }) { + return _then(_value.copyWith( + currentLabel: null == currentLabel + ? _value.currentLabel + : currentLabel // ignore: cast_nullable_to_non_nullable + as String, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeCommonScaffoldSelectorStateImplCopyWith<$Res> + implements $HomeCommonScaffoldSelectorStateCopyWith<$Res> { + factory _$$HomeCommonScaffoldSelectorStateImplCopyWith( + _$HomeCommonScaffoldSelectorStateImpl value, + $Res Function(_$HomeCommonScaffoldSelectorStateImpl) then) = + __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String currentLabel, String? locale}); +} + +/// @nodoc +class __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl<$Res> + extends _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, + _$HomeCommonScaffoldSelectorStateImpl> + implements _$$HomeCommonScaffoldSelectorStateImplCopyWith<$Res> { + __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl( + _$HomeCommonScaffoldSelectorStateImpl _value, + $Res Function(_$HomeCommonScaffoldSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentLabel = null, + Object? locale = freezed, + }) { + return _then(_$HomeCommonScaffoldSelectorStateImpl( + currentLabel: null == currentLabel + ? _value.currentLabel + : currentLabel // ignore: cast_nullable_to_non_nullable + as String, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$HomeCommonScaffoldSelectorStateImpl + implements _HomeCommonScaffoldSelectorState { + const _$HomeCommonScaffoldSelectorStateImpl( + {required this.currentLabel, required this.locale}); + + @override + final String currentLabel; + @override + final String? locale; + + @override + String toString() { + return 'HomeCommonScaffoldSelectorState(currentLabel: $currentLabel, locale: $locale)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeCommonScaffoldSelectorStateImpl && + (identical(other.currentLabel, currentLabel) || + other.currentLabel == currentLabel) && + (identical(other.locale, locale) || other.locale == locale)); + } + + @override + int get hashCode => Object.hash(runtimeType, currentLabel, locale); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeCommonScaffoldSelectorStateImplCopyWith< + _$HomeCommonScaffoldSelectorStateImpl> + get copyWith => __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl< + _$HomeCommonScaffoldSelectorStateImpl>(this, _$identity); +} + +abstract class _HomeCommonScaffoldSelectorState + implements HomeCommonScaffoldSelectorState { + const factory _HomeCommonScaffoldSelectorState( + {required final String currentLabel, + required final String? locale}) = _$HomeCommonScaffoldSelectorStateImpl; + + @override + String get currentLabel; + @override + String? get locale; + @override + @JsonKey(ignore: true) + _$$HomeCommonScaffoldSelectorStateImplCopyWith< + _$HomeCommonScaffoldSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$HomeNavigationSelectorState { + int get currentIndex => throw _privateConstructorUsedError; + String? get locale => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeNavigationSelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeNavigationSelectorStateCopyWith<$Res> { + factory $HomeNavigationSelectorStateCopyWith( + HomeNavigationSelectorState value, + $Res Function(HomeNavigationSelectorState) then) = + _$HomeNavigationSelectorStateCopyWithImpl<$Res, + HomeNavigationSelectorState>; + @useResult + $Res call({int currentIndex, String? locale}); +} + +/// @nodoc +class _$HomeNavigationSelectorStateCopyWithImpl<$Res, + $Val extends HomeNavigationSelectorState> + implements $HomeNavigationSelectorStateCopyWith<$Res> { + _$HomeNavigationSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentIndex = null, + Object? locale = freezed, + }) { + return _then(_value.copyWith( + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeNavigationSelectorStateImplCopyWith<$Res> + implements $HomeNavigationSelectorStateCopyWith<$Res> { + factory _$$HomeNavigationSelectorStateImplCopyWith( + _$HomeNavigationSelectorStateImpl value, + $Res Function(_$HomeNavigationSelectorStateImpl) then) = + __$$HomeNavigationSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int currentIndex, String? locale}); +} + +/// @nodoc +class __$$HomeNavigationSelectorStateImplCopyWithImpl<$Res> + extends _$HomeNavigationSelectorStateCopyWithImpl<$Res, + _$HomeNavigationSelectorStateImpl> + implements _$$HomeNavigationSelectorStateImplCopyWith<$Res> { + __$$HomeNavigationSelectorStateImplCopyWithImpl( + _$HomeNavigationSelectorStateImpl _value, + $Res Function(_$HomeNavigationSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentIndex = null, + Object? locale = freezed, + }) { + return _then(_$HomeNavigationSelectorStateImpl( + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + locale: freezed == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$HomeNavigationSelectorStateImpl + implements _HomeNavigationSelectorState { + const _$HomeNavigationSelectorStateImpl( + {required this.currentIndex, required this.locale}); + + @override + final int currentIndex; + @override + final String? locale; + + @override + String toString() { + return 'HomeNavigationSelectorState(currentIndex: $currentIndex, locale: $locale)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeNavigationSelectorStateImpl && + (identical(other.currentIndex, currentIndex) || + other.currentIndex == currentIndex) && + (identical(other.locale, locale) || other.locale == locale)); + } + + @override + int get hashCode => Object.hash(runtimeType, currentIndex, locale); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeNavigationSelectorStateImplCopyWith<_$HomeNavigationSelectorStateImpl> + get copyWith => __$$HomeNavigationSelectorStateImplCopyWithImpl< + _$HomeNavigationSelectorStateImpl>(this, _$identity); +} + +abstract class _HomeNavigationSelectorState + implements HomeNavigationSelectorState { + const factory _HomeNavigationSelectorState( + {required final int currentIndex, + required final String? locale}) = _$HomeNavigationSelectorStateImpl; + + @override + int get currentIndex; + @override + String? get locale; + @override + @JsonKey(ignore: true) + _$$HomeNavigationSelectorStateImplCopyWith<_$HomeNavigationSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/version.freezed.dart b/lib/models/generated/version.freezed.dart new file mode 100644 index 0000000..c508073 --- /dev/null +++ b/lib/models/generated/version.freezed.dart @@ -0,0 +1,171 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../version.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +VersionInfo _$VersionInfoFromJson(Map json) { + return _VersionInfo.fromJson(json); +} + +/// @nodoc +mixin _$VersionInfo { + String get clashName => throw _privateConstructorUsedError; + String get version => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $VersionInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VersionInfoCopyWith<$Res> { + factory $VersionInfoCopyWith( + VersionInfo value, $Res Function(VersionInfo) then) = + _$VersionInfoCopyWithImpl<$Res, VersionInfo>; + @useResult + $Res call({String clashName, String version}); +} + +/// @nodoc +class _$VersionInfoCopyWithImpl<$Res, $Val extends VersionInfo> + implements $VersionInfoCopyWith<$Res> { + _$VersionInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clashName = null, + Object? version = null, + }) { + return _then(_value.copyWith( + clashName: null == clashName + ? _value.clashName + : clashName // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$VersionInfoImplCopyWith<$Res> + implements $VersionInfoCopyWith<$Res> { + factory _$$VersionInfoImplCopyWith( + _$VersionInfoImpl value, $Res Function(_$VersionInfoImpl) then) = + __$$VersionInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String clashName, String version}); +} + +/// @nodoc +class __$$VersionInfoImplCopyWithImpl<$Res> + extends _$VersionInfoCopyWithImpl<$Res, _$VersionInfoImpl> + implements _$$VersionInfoImplCopyWith<$Res> { + __$$VersionInfoImplCopyWithImpl( + _$VersionInfoImpl _value, $Res Function(_$VersionInfoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clashName = null, + Object? version = null, + }) { + return _then(_$VersionInfoImpl( + clashName: null == clashName + ? _value.clashName + : clashName // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$VersionInfoImpl implements _VersionInfo { + const _$VersionInfoImpl({this.clashName = "", this.version = ""}); + + factory _$VersionInfoImpl.fromJson(Map json) => + _$$VersionInfoImplFromJson(json); + + @override + @JsonKey() + final String clashName; + @override + @JsonKey() + final String version; + + @override + String toString() { + return 'VersionInfo(clashName: $clashName, version: $version)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VersionInfoImpl && + (identical(other.clashName, clashName) || + other.clashName == clashName) && + (identical(other.version, version) || other.version == version)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, clashName, version); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$VersionInfoImplCopyWith<_$VersionInfoImpl> get copyWith => + __$$VersionInfoImplCopyWithImpl<_$VersionInfoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$VersionInfoImplToJson( + this, + ); + } +} + +abstract class _VersionInfo implements VersionInfo { + const factory _VersionInfo({final String clashName, final String version}) = + _$VersionInfoImpl; + + factory _VersionInfo.fromJson(Map json) = + _$VersionInfoImpl.fromJson; + + @override + String get clashName; + @override + String get version; + @override + @JsonKey(ignore: true) + _$$VersionInfoImplCopyWith<_$VersionInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/version.g.dart b/lib/models/generated/version.g.dart new file mode 100644 index 0000000..8d92bcb --- /dev/null +++ b/lib/models/generated/version.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../version.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$VersionInfoImpl _$$VersionInfoImplFromJson(Map json) => + _$VersionInfoImpl( + clashName: json['clashName'] as String? ?? "", + version: json['version'] as String? ?? "", + ); + +Map _$$VersionInfoImplToJson(_$VersionInfoImpl instance) => + { + 'clashName': instance.clashName, + 'version': instance.version, + }; diff --git a/lib/models/log.dart b/lib/models/log.dart new file mode 100644 index 0000000..3d712d2 --- /dev/null +++ b/lib/models/log.dart @@ -0,0 +1,33 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'generated/log.g.dart'; + +@JsonSerializable() +class Log { + @JsonKey(name: "LogLevel") + LogLevel logLevel; + @JsonKey(name: "Payload") + String? payload; + DateTime _dateTime; + + Log({ + required this.logLevel, + this.payload, + }) : _dateTime = DateTime.now(); + + DateTime get dateTime => _dateTime; + + factory Log.fromJson(Map json) { + return _$LogFromJson(json); + } + + Map toJson() { + return _$LogToJson(this); + } + + @override + String toString() { + return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}'; + } +} diff --git a/lib/models/models.dart b/lib/models/models.dart new file mode 100644 index 0000000..8e88abd --- /dev/null +++ b/lib/models/models.dart @@ -0,0 +1,15 @@ +export 'app.dart'; +export 'clash_config.dart'; +export 'config.dart'; +export 'profile.dart'; +export 'proxy.dart'; +export 'version.dart'; +export 'traffic.dart'; +export 'log.dart'; +export 'system_color_scheme.dart'; +export 'connection.dart'; +export 'package.dart'; +export 'common.dart'; +export 'ffi.dart'; +export 'selector.dart'; +export 'navigation.dart'; \ No newline at end of file diff --git a/lib/models/navigation.dart b/lib/models/navigation.dart new file mode 100644 index 0000000..e38c471 --- /dev/null +++ b/lib/models/navigation.dart @@ -0,0 +1,18 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/navigation.freezed.dart'; + +@freezed +class NavigationItem with _$NavigationItem { + const factory NavigationItem({ + required Icon icon, + required String label, + final String? description, + required Widget fragment, + String? path, + @Default([NavigationItemMode.mobile, NavigationItemMode.desktop]) + List modes, + }) = _NavigationItem; +} diff --git a/lib/models/package.dart b/lib/models/package.dart new file mode 100644 index 0000000..abf406f --- /dev/null +++ b/lib/models/package.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/package.g.dart'; +part 'generated/package.freezed.dart'; + +@freezed +class Package with _$Package { + const factory Package({ + required String packageName, + required String label, + required bool isSystem, + }) = _Package; + + factory Package.fromJson(Map json) => + _$PackageFromJson(json); +} diff --git a/lib/models/profile.dart b/lib/models/profile.dart new file mode 100644 index 0000000..18a7819 --- /dev/null +++ b/lib/models/profile.dart @@ -0,0 +1,208 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'common.dart'; + +part 'generated/profile.g.dart'; + +@JsonSerializable() +class UserInfo { + int upload; + int download; + int total; + int? expire; + + UserInfo({ + int? upload, + int? download, + int? total, + this.expire, + }) : upload = upload ?? 0, + download = download ?? 0, + total = total ?? 0; + + Map toJson() { + return _$UserInfoToJson(this); + } + + factory UserInfo.fromJson(Map json) { + return _$UserInfoFromJson(json); + } + + factory UserInfo.formHString(String? info) { + if (info == null) return UserInfo(); + var list = info.split(";"); + Map map = {}; + for (var i in list) { + var keyValue = i.trim().split("="); + map[keyValue[0]] = int.tryParse(keyValue[1]); + } + return UserInfo( + upload: map["upload"], + download: map["download"], + total: map["total"], + expire: map["expire"], + ); + } + + @override + String toString() { + return 'UserInfo{upload: $upload, download: $download, total: $total, expire: $expire}'; + } +} + +@JsonSerializable() +class Profile { + String id; + String? label; + String? groupName; + String? proxyName; + String? url; + DateTime? lastUpdateDate; + Duration autoUpdateDuration; + UserInfo? userInfo; + bool autoUpdate; + + Profile({ + String? id, + this.label, + this.url, + this.userInfo, + this.groupName, + this.proxyName, + this.lastUpdateDate, + Duration? autoUpdateDuration, + this.autoUpdate = true, + }) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(), + autoUpdateDuration = + autoUpdateDuration ?? appConstant.defaultUpdateDuration; + + ProfileType get type => url == null ? ProfileType.file : ProfileType.url; + + Future> update() async { + if (url == null) { + return Result.error( + message: appLocalizations.unableToUpdateCurrentProfileDesc, + ); + } + final responseResult = await Request.getFileResponseForUrl(url!); + final response = responseResult.data; + if (responseResult.type != ResultType.success || response == null) { + return Result.error(message: responseResult.message); + } + final disposition = response.headers['content-disposition']; + if (disposition != null && label == null) { + final parseValue = HeaderValue.parse(disposition); + parseValue.parameters.forEach( + (key, value) { + if (key.startsWith("filename")) { + if (key == "filename*") { + label = Uri.decodeComponent((value ?? "").split("'").last); + } else { + label = value ?? id; + } + } + }, + ); + } + final userinfo = response.headers['subscription-userinfo']; + userInfo = UserInfo.formHString(userinfo); + final saveResult = await saveFile(response.bodyBytes); + if (saveResult.type == ResultType.error) { + return Result.error(message: saveResult.message); + } + lastUpdateDate = DateTime.now(); + return Result.success(); + } + + Future check() async { + final profilePath = await appPath.getProfilePath(id); + return await File(profilePath!).exists(); + } + + Future> saveFile(Uint8List bytes) async { + final isValidate = clashCore.validateConfig(utf8.decode(bytes)); + if (!isValidate) { + return Result.error(message: appLocalizations.profileParseErrorDesc); + } + final path = await appPath.getProfilePath(id); + final file = File(path!); + final isExists = await file.exists(); + if (!isExists) { + await file.create(recursive: true); + } + await file.writeAsBytes(bytes); + lastUpdateDate = DateTime.now(); + return Result.success(); + } + + Map toJson() { + return _$ProfileToJson(this); + } + + factory Profile.fromJson(Map json) { + return _$ProfileFromJson(json); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Profile && + runtimeType == other.runtimeType && + id == other.id && + label == other.label && + groupName == other.groupName && + proxyName == other.proxyName && + url == other.url && + lastUpdateDate == other.lastUpdateDate && + autoUpdateDuration == other.autoUpdateDuration && + userInfo == other.userInfo && + autoUpdate == other.autoUpdate; + + @override + int get hashCode => + id.hashCode ^ + label.hashCode ^ + groupName.hashCode ^ + proxyName.hashCode ^ + url.hashCode ^ + lastUpdateDate.hashCode ^ + autoUpdateDuration.hashCode ^ + userInfo.hashCode ^ + autoUpdate.hashCode; + + @override + String toString() { + return 'Profile{id: $id, label: $label, groupName: $groupName, proxyName: $proxyName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate}'; + } + + Profile copyWith({ + String? label, + String? url, + UserInfo? userInfo, + String? groupName, + String? proxyName, + DateTime? lastUpdateDate, + Duration? autoUpdateDuration, + bool? autoUpdate, + }) { + return Profile( + id: id, + label: label ?? this.label, + url: url ?? this.url, + groupName: groupName ?? this.groupName, + proxyName: proxyName ?? this.proxyName, + userInfo: userInfo ?? this.userInfo, + lastUpdateDate: lastUpdateDate ?? this.lastUpdateDate, + autoUpdateDuration: autoUpdateDuration ?? this.autoUpdateDuration, + autoUpdate: autoUpdate ?? this.autoUpdate, + ); + } +} diff --git a/lib/models/proxy.dart b/lib/models/proxy.dart new file mode 100644 index 0000000..c65901d --- /dev/null +++ b/lib/models/proxy.dart @@ -0,0 +1,31 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../enum/enum.dart'; + +part 'generated/proxy.g.dart'; + +part 'generated/proxy.freezed.dart'; + +typedef DelayMap = Map; + +@freezed +class Group with _$Group { + const factory Group({ + required GroupType type, + @Default([]) List all, + String? now, + required String name, + }) = _Group; + + factory Group.fromJson(Map json) => _$GroupFromJson(json); +} + +@freezed +class Proxy with _$Proxy { + const factory Proxy({ + @Default("") String name, + @Default("") String type, + }) = _Proxy; + + factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); +} diff --git a/lib/models/selector.dart b/lib/models/selector.dart new file mode 100644 index 0000000..469a559 --- /dev/null +++ b/lib/models/selector.dart @@ -0,0 +1,103 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'config.dart'; +import 'navigation.dart'; +import 'package.dart'; +import 'profile.dart'; + +part 'generated/selector.freezed.dart'; + +@freezed +class StartButtonSelectorState with _$StartButtonSelectorState { + const factory StartButtonSelectorState({ + required bool isInit, + required bool hasProfile, + }) = _StartButtonSelectorState; +} + +@freezed +class UpdateCurrentDelaySelectorState with _$UpdateCurrentDelaySelectorState { + const factory UpdateCurrentDelaySelectorState({ + required String? currentProxyName, + required bool isCurrent, + required int? delay, + required bool isInit, + }) = _UpdateCurrentDelaySelectorState; +} + +@freezed +class NetworkDetectionSelectorState with _$NetworkDetectionSelectorState { + const factory NetworkDetectionSelectorState({ + required String? currentProxyName, + required int? delay, + required bool isInit, + }) = _NetworkDetectionSelectorState; +} + +@freezed +class ProfilesSelectorState with _$ProfilesSelectorState { + const factory ProfilesSelectorState({ + required List profiles, + required String? currentProfileId, + }) = _ProfilesSelectorState; +} + +@freezed +class PackageListSelectorState with _$PackageListSelectorState { + const factory PackageListSelectorState({ + required AccessControl accessControl, + required List packages, + }) = _PackageListSelectorState; +} + +@freezed +class ApplicationSelectorState with _$ApplicationSelectorState { + const factory ApplicationSelectorState({ + String? locale, + ThemeMode? themeMode, + int? primaryColor, + }) = _ApplicationSelectorState; +} + +@freezed +class HomeLayoutSelectorState with _$HomeLayoutSelectorState{ + const factory HomeLayoutSelectorState({ + required List navigationItems, + required int currentIndex, + })=_HomeLayoutSelectorState; +} + +@freezed +class TrayContainerSelectorState with _$TrayContainerSelectorState{ + const factory TrayContainerSelectorState({ + required Mode mode, + required bool autoLaunch, + required bool isRun, + required String? locale, + })=_TrayContainerSelectorState; +} + +@freezed +class UpdateNavigationsSelector with _$UpdateNavigationsSelector{ + const factory UpdateNavigationsSelector({ + required bool openLogs, + required bool hasProxies, + }) = _UpdateNavigationsSelector; +} + +@freezed +class HomeCommonScaffoldSelectorState with _$HomeCommonScaffoldSelectorState { + const factory HomeCommonScaffoldSelectorState({ + required String currentLabel, + required String? locale, + }) = _HomeCommonScaffoldSelectorState; +} + +@freezed +class HomeNavigationSelectorState with _$HomeNavigationSelectorState{ + const factory HomeNavigationSelectorState({ + required int currentIndex, + required String? locale, + }) = _HomeNavigationSelectorState; +} diff --git a/lib/models/system_color_scheme.dart b/lib/models/system_color_scheme.dart new file mode 100644 index 0000000..25af319 --- /dev/null +++ b/lib/models/system_color_scheme.dart @@ -0,0 +1,24 @@ +import 'package:fl_clash/common/constant.dart'; +import 'package:flutter/material.dart'; + +class SystemColorSchemes { + SystemColorSchemes({ + ColorScheme? lightColorScheme, + ColorScheme? darkColorScheme, + }) : lightColorScheme = lightColorScheme ?? + ColorScheme.fromSeed(seedColor: appConstant.defaultPrimaryColor), + darkColorScheme = darkColorScheme ?? + ColorScheme.fromSeed( + seedColor: appConstant.defaultPrimaryColor, + brightness: Brightness.dark, + ); + ColorScheme lightColorScheme; + ColorScheme darkColorScheme; + + getSystemColorSchemeForBrightness(Brightness? brightness) { + if (brightness != null && brightness == Brightness.dark) { + return darkColorScheme; + } + return lightColorScheme; + } +} diff --git a/lib/models/traffic.dart b/lib/models/traffic.dart new file mode 100644 index 0000000..4558ca8 --- /dev/null +++ b/lib/models/traffic.dart @@ -0,0 +1,112 @@ +import 'dart:math'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +class Traffic { + int id; + TrafficValue up; + TrafficValue down; + + Traffic({num? up, num? down}) + : id = DateTime.now().millisecondsSinceEpoch, + up = TrafficValue(value: up), + down = TrafficValue(value: down); + + num get speed => up.value + down.value; + + factory Traffic.fromMap(Map map) { + return Traffic( + up: map['up'], + down: map['down'], + ); + } + + @override + String toString() { + return '$up↑ $down↓'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Traffic && + runtimeType == other.runtimeType && + id == other.id && + up == other.up && + down == other.down; + + @override + int get hashCode => id.hashCode ^ up.hashCode ^ down.hashCode; +} + +@immutable +class TrafficValueShow { + final String value; + final TrafficUnit unit; + + const TrafficValueShow({ + required this.value, + required this.unit, + }); +} + +@immutable +class TrafficValue { + final num _value; + + const TrafficValue({num? value}) : _value = value ?? 0; + + num get value => _value; + + String get show => "$showValue $showUnit"; + + String get showValue => trafficValueShow.value; + + String get showUnit => trafficValueShow.unit.name; + + TrafficValueShow get trafficValueShow { + if (_value > pow(1024, 4)) { + return TrafficValueShow( + value: (_value / pow(1024, 4)).fixed(), + unit: TrafficUnit.TB, + ); + } + if (_value > pow(1024, 3)) { + return TrafficValueShow( + value: (_value / pow(1024, 3)).fixed(), + unit: TrafficUnit.GB, + ); + } + if (_value > pow(1024, 2)) { + return TrafficValueShow( + value: (_value / pow(1024, 2)).fixed(), unit: TrafficUnit.MB); + } + if (_value > pow(1024, 1)) { + return TrafficValueShow( + value: (_value / pow(1024, 1)).fixed(), + unit: TrafficUnit.KB, + ); + } + return TrafficValueShow( + value: _value.fixed(), + unit: TrafficUnit.B, + ); + } + + @override + String toString() { + return "$showValue$showUnit"; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TrafficValue && + runtimeType == other.runtimeType && + _value == other._value; + + @override + int get hashCode => _value.hashCode; +} diff --git a/lib/models/version.dart b/lib/models/version.dart new file mode 100644 index 0000000..7de99b2 --- /dev/null +++ b/lib/models/version.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/version.g.dart'; + +part 'generated/version.freezed.dart'; + +@freezed +class VersionInfo with _$VersionInfo { + const factory VersionInfo({ + @Default("") String clashName, + @Default("") String version, + }) = _VersionInfo; + + factory VersionInfo.fromJson(Map json) => + _$VersionInfoFromJson(json); +} diff --git a/lib/pages/home.dart b/lib/pages/home.dart new file mode 100644 index 0000000..a120993 --- /dev/null +++ b/lib/pages/home.dart @@ -0,0 +1,207 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import '../enum/enum.dart'; +import '../widgets/widgets.dart'; + +typedef OnSelected = void Function(int index); + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + Widget _buildBody({ + required List navigationItems, + }) { + globalState.currentNavigationItems = navigationItems; + return Selector( + selector: (_, appState) { + final index = navigationItems.lastIndexWhere( + (element) => element.label == appState.currentLabel, + ); + return index == -1 ? 0 : index; + }, + builder: (context, currentIndex, __) { + if (globalState.pageController != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.appController.toPage(currentIndex, hasAnimate: true); + }); + } else { + globalState.pageController = PageController( + initialPage: currentIndex, + keepPage: true, + ); + } + return PageView.builder( + controller: globalState.pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: navigationItems.length, + itemBuilder: (_, index) { + final navigationItem = navigationItems[index]; + return KeepContainer( + child: navigationItem.fragment, + ); + }, + ); + }, + ); + } + + _buildNavigationRail({ + required List navigationItems, + bool extended = false, + }) { + return Selector2( + selector: (_, appState, config) { + final index = navigationItems.lastIndexWhere( + (element) => element.label == appState.currentLabel, + ); + return HomeNavigationSelectorState( + currentIndex: index == -1 ? 0 : index, + locale: config.locale, + ); + }, + builder: (context, state, __) { + return AdaptiveScaffold.standardNavigationRail( + onDestinationSelected: context.appController.toPage, + destinations: navigationItems + .map( + (e) => NavigationRailDestination( + icon: e.icon, + label: Text( + Intl.message(e.label), + ), + ), + ) + .toList(), + extended: extended, + width: extended ? 160 : 80, + selectedIndex: state.currentIndex, + labelType: extended + ? NavigationRailLabelType.none + : NavigationRailLabelType.selected, + ); + }, + ); + } + + _buildBottomNavigationBar({ + required List navigationItems, + }) { + return Selector2( + selector: (_, appState, config) { + final index = navigationItems.lastIndexWhere( + (element) => element.label == appState.currentLabel, + ); + return HomeNavigationSelectorState( + currentIndex: index == -1 ? 0 : index, + locale: config.locale, + ); + }, + builder: (context, state, __) { + final mobileDestinations = navigationItems + .map( + (e) => NavigationDestination( + icon: e.icon, + label: Intl.message(e.label), + ), + ) + .toList(); + return AdaptiveScaffold.standardBottomNavigationBar( + destinations: mobileDestinations, + onDestinationSelected: context.appController.toPage, + currentIndex: state.currentIndex, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return PopContainer( + child: Selector2( + selector: (_, appState, config) => HomeCommonScaffoldSelectorState( + currentLabel: appState.currentLabel, + locale: config.locale, + ), + builder: (_, state, child) { + return CommonScaffold( + key: globalState.homeScaffoldKey, + title: Text( + Intl.message(state.currentLabel), + ), + body: child!, + ); + }, + child: Selector>( + selector: (_, appState) => appState.navigationItems, + builder: (_, navigationItems, __) { + debugPrint("[Home] update===>"); + final desktopNavigationItems = navigationItems + .where( + (element) => + element.modes.contains(NavigationItemMode.desktop), + ) + .toList(); + final mobileNavigationItems = navigationItems + .where( + (element) => + element.modes.contains(NavigationItemMode.mobile), + ) + .toList(); + return AdaptiveLayout( + transitionDuration: kThemeAnimationDuration, + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primary_navigation_medium'), + builder: (_) => _buildNavigationRail( + navigationItems: desktopNavigationItems, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primary_navigation_large'), + builder: (_) => _buildNavigationRail( + navigationItems: desktopNavigationItems, + extended: true, + ), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.mediumAndUp: SlotLayout.from( + key: const Key('body_mediumAndUp'), + builder: (_) => _buildBody( + navigationItems: desktopNavigationItems, + ), + ), + Breakpoints.small: SlotLayout.from( + key: const Key('body_small'), + builder: (_) => _buildBody( + navigationItems: mobileNavigationItems, + ), + ) + }, + ), + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottom_navigation_small'), + builder: (_) => _buildBottomNavigationBar( + navigationItems: mobileNavigationItems, + ), + ) + }, + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/pages.dart b/lib/pages/pages.dart new file mode 100644 index 0000000..ef42ae9 --- /dev/null +++ b/lib/pages/pages.dart @@ -0,0 +1,2 @@ +export 'home.dart'; +export 'scan.dart'; \ No newline at end of file diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart new file mode 100644 index 0000000..e48d252 --- /dev/null +++ b/lib/pages/scan.dart @@ -0,0 +1,200 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class ScanPage extends StatefulWidget { + const ScanPage({super.key}); + + @override + State createState() => _ScanPageState(); +} + +class _ScanPageState extends State with WidgetsBindingObserver { + MobileScannerController controller = MobileScannerController( + detectionSpeed: DetectionSpeed.noDuplicates, + formats: const [BarcodeFormat.qrCode], + ); + + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _subscription = controller.barcodes.listen(_handleBarcode); + unawaited(controller.start()); + } + + _handleBarcode(BarcodeCapture barcodeCapture) { + final barcode = barcodeCapture.barcodes.first; + if (barcode.type == BarcodeType.url) { + Navigator.pop( + context, + barcode.rawValue, + ); + } else { + Navigator.pop(context); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + case AppLifecycleState.inactive: + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(controller.stop()); + } + } + + @override + Widget build(BuildContext context) { + double sideLength = min(400, MediaQuery.of(context).size.width * 0.67); + final scanWindow = Rect.fromCenter( + center: MediaQuery.sizeOf(context).center(Offset.zero), + width: sideLength, + height: sideLength, + ); + return Scaffold( + body: Stack( + children: [ + Center( + child: MobileScanner( + controller: controller, + scanWindow: scanWindow, + ), + ), + CustomPaint( + painter: ScannerOverlay(scanWindow: scanWindow), + ), + AppBar( + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + leading: IconButton( + style: const ButtonStyle( + iconSize: MaterialStatePropertyAll(32), + foregroundColor: MaterialStatePropertyAll(Colors.white), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.close), + ), + ), + Container( + margin: const EdgeInsets.only(bottom: 32), + alignment: Alignment.bottomCenter, + child: ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, _) { + var icon = const Icon(Icons.flash_off); + var backgroundColor = Colors.black12; + switch (state.torchState) { + case TorchState.off: + icon = const Icon(Icons.flash_off); + backgroundColor = Colors.black12; + case TorchState.on: + icon = const Icon(Icons.flash_on); + backgroundColor = Colors.orange; + case TorchState.unavailable: + icon = const Icon(Icons.no_flash); + backgroundColor = Colors.grey; + } + return IconButton( + color: Colors.white, + icon: icon, + style: ButtonStyle( + foregroundColor: + const MaterialStatePropertyAll(Colors.white), + backgroundColor: MaterialStatePropertyAll(backgroundColor), + ), + padding: const EdgeInsets.all(16), + iconSize: 32.0, + onPressed: () => controller.toggleTorch(), + ); + }, + ), + ), + ], + ), + ); + } + + @override + Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + unawaited(_subscription?.cancel()); + _subscription = null; + super.dispose(); + await controller.dispose(); + } +} + +class ScannerOverlay extends CustomPainter { + const ScannerOverlay({ + required this.scanWindow, + this.borderRadius = 12.0, + }); + + final Rect scanWindow; + final double borderRadius; + + @override + void paint(Canvas canvas, Size size) { + final backgroundPath = Path()..addRect(Rect.largest); + + final cutoutPath = Path() + ..addRRect( + RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ), + ); + + final backgroundPaint = Paint() + ..color = Colors.black.withOpacity(0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final backgroundWithCutout = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + + final borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 4.0; + + final borderRect = RRect.fromRectAndCorners( + scanWindow, + topLeft: Radius.circular(borderRadius), + topRight: Radius.circular(borderRadius), + bottomLeft: Radius.circular(borderRadius), + bottomRight: Radius.circular(borderRadius), + ); + + canvas.drawPath(backgroundWithCutout, backgroundPaint); + canvas.drawRRect(borderRect, borderPaint); + } + + @override + bool shouldRepaint(ScannerOverlay oldDelegate) { + return scanWindow != oldDelegate.scanWindow || + borderRadius != oldDelegate.borderRadius; + } +} \ No newline at end of file diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart new file mode 100644 index 0000000..69da37d --- /dev/null +++ b/lib/plugins/app.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class App { + static App? _instance; + MethodChannel? methodChannel; + Function()? onExit; + + App._internal() { + if (Platform.isAndroid) { + methodChannel = const MethodChannel("app"); + methodChannel!.setMethodCallHandler((call) async { + switch (call.method) { + case "exit": + if (onExit != null) { + await onExit!(); + } + break; + default: + throw MissingPluginException(); + } + }); + } + } + + setOnExit(Function() onExit) { + this.onExit = onExit; + } + + factory App() { + _instance ??= App._internal(); + return _instance!; + } + + Future moveTaskToBack() async { + return await methodChannel?.invokeMethod("moveTaskToBack"); + } + + Future> getPackages() async { + final packagesString = + await methodChannel?.invokeMethod("getPackages"); + final List packagesRaw = + packagesString != null ? json.decode(packagesString) : []; + return packagesRaw.map((e) => Package.fromJson(e)).toList(); + } + + Future getPackageIcon(String packageName) async { + final base64 = await methodChannel?.invokeMethod("getPackageIcon", { + "packageName": packageName, + }); + if (base64 == null) { + return null; + } + return MemoryImage(base64Decode(base64)); + } + + Future tip(String? message) async { + return await methodChannel?.invokeMethod("tip", { + "message": "$message", + }); + } + + Future getPackageName(Metadata metadata) async { + return await methodChannel?.invokeMethod("getPackageName", { + "data": json.encode(metadata), + }); + } +} + +final app = Platform.isAndroid ? App() : null; diff --git a/lib/plugins/proxy.dart b/lib/plugins/proxy.dart new file mode 100644 index 0000000..c4e62e2 --- /dev/null +++ b/lib/plugins/proxy.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/services.dart'; +import 'package:proxy/proxy_platform_interface.dart'; + +class Proxy extends ProxyPlatform { + static Proxy? _instance; + late MethodChannel methodChannel; + late ReceivePort receiver; + + Proxy._internal() { + methodChannel = const MethodChannel("proxy"); + receiver = ReceivePort() + ..listen( + (message) { + setProtect(int.parse(message)); + }, + ); + methodChannel.setMethodCallHandler((call) async { + switch (call.method) { + case "startAfter": + int fd = call.arguments; + startAfterHook(fd); + break; + default: + throw MissingPluginException(); + } + }); + } + + factory Proxy() { + _instance ??= Proxy._internal(); + return _instance!; + } + + @override + Future startProxy(int port, String? args) async { + return await methodChannel + .invokeMethod("StartProxy", {'port': port, 'args': args}); + } + + @override + Future stopProxy() async { + clashCore.stopTun(); + final isStop = await methodChannel.invokeMethod("StopProxy"); + if (isStop == true) { + startTime = null; + } + return isStop; + } + + Future setProtect(int fd) async { + return await methodChannel.invokeMethod("SetProtect", {'fd': fd}); + } + + Future getRunTimeStamp() async { + return await methodChannel.invokeMethod("GetRunTimeStamp"); + } + + Future startForeground({ + required String title, + required String content, + }) async { + return await methodChannel.invokeMethod("startForeground", { + 'title': title, + 'content': content, + }); + } + + bool get isStart => startTime != null && startTime!.isBeforeNow(); + + startAfterHook(int? fd) { + if (!isStart && fd != null) { + clashCore.startTun(fd); + updateStartTime(); + } + } + + updateStartTime() async { + startTime = await getRunTime(); + } + + Future getRunTime() async { + final runTimeStamp = await getRunTimeStamp(); + return runTimeStamp != null + ? DateTime.fromMillisecondsSinceEpoch(runTimeStamp) + : null; + } +} + +final proxy = Platform.isAndroid ? Proxy() : null; diff --git a/lib/plugins/tile.dart b/lib/plugins/tile.dart new file mode 100644 index 0000000..c37c4d4 --- /dev/null +++ b/lib/plugins/tile.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +abstract mixin class TileListener { + void onStart() {} + + void onStop() {} + + void onDetached(){ + + } +} + +class Tile { + StreamSubscription? subscription; + + final MethodChannel _channel = const MethodChannel('tile'); + + Tile._() { + _channel.setMethodCallHandler(_methodCallHandler); + } + + static final Tile instance = Tile._(); + + final ObserverList _listeners = ObserverList(); + + Future _methodCallHandler(MethodCall call) async { + for (final TileListener listener in _listeners) { + switch (call.method) { + case "start": + listener.onStart(); + break; + case "stop": + listener.onStop(); + break; + case "detached": + listener.onDetached(); + break; + } + } + } + + bool get hasListeners { + return _listeners.isNotEmpty; + } + + void addListener(TileListener listener) { + _listeners.add(listener); + } + + void removeListener(TileListener listener) { + _listeners.remove(listener); + } +} + +final tile = Platform.isAndroid ? Tile.instance : null; diff --git a/lib/router/fade_page.dart b/lib/router/fade_page.dart new file mode 100644 index 0000000..e78ad7f --- /dev/null +++ b/lib/router/fade_page.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class FadePage extends Page { + final Widget child; + final bool maintainState; + final bool fullscreenDialog; + final bool allowSnapshotting; + + const FadePage({ + required this.child, + this.maintainState = true, + this.fullscreenDialog = false, + this.allowSnapshotting = true, + super.key, + super.name, + super.arguments, + super.restorationId, + }); + + @override + Route createRoute(BuildContext context) { + return FadePageRoute(page: this); + } +} + +class FadePageRoute extends PageRoute { + final FadePage page; + + FadePageRoute({ + required this.page, + }) : super(settings: page); + + FadePage get _page => settings as FadePage; + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return _page.child; + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + } + + @override + Duration get transitionDuration => const Duration(milliseconds: 600); + + @override + bool get maintainState => false; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; +} diff --git a/lib/state.dart b/lib/state.dart new file mode 100644 index 0000000..b60ed7a --- /dev/null +++ b/lib/state.dart @@ -0,0 +1,215 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:animations/animations.dart'; +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/plugins/app.dart'; +import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:flutter/material.dart'; + +import 'models/models.dart'; +import 'common/common.dart'; + +class GlobalState { + Timer? timer; + Timer? currentDelayTimer; + Function? updateCurrentDelayDebounce; + PageController? pageController; + final navigatorKey = GlobalKey(); + final Map packageNameMap = {}; + GlobalKey homeScaffoldKey = GlobalKey(); + List updateFunctionLists = []; + List currentNavigationItems = []; + bool updatePackagesLock = false; + + startListenUpdate() { + if (timer != null && timer!.isActive == true) return; + timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + for (final function in updateFunctionLists) { + function(); + } + }); + } + + stopListenUpdate() { + if (timer == null || timer?.isActive == false) return; + timer?.cancel(); + } + + Future updateClashConfig({ + required ClashConfig clashConfig, + required Config config, + bool isPatch = true, + }) async { + final profilePath = await appPath.getProfilePath(config.currentProfileId); + debugPrint("update config"); + return clashCore.updateConfig(UpdateConfigParams( + profilePath: profilePath, + config: clashConfig, + isPatch: isPatch, + )); + } + + updateCoreVersionInfo(AppState appState) { + appState.versionInfo = clashCore.getVersionInfo(); + } + + Future startSystemProxy({ + required Config config, + required ClashConfig clashConfig, + }) async { + final args = + config.isAccessControl ? json.encode(config.accessControl) : null; + await proxyManager.startProxy( + port: clashConfig.mixedPort, + args: args, + ); + startListenUpdate(); + } + + Future stopSystemProxy() async { + await proxyManager.stopProxy(); + stopListenUpdate(); + } + + void updateCurrentDelay( + String? proxyName, + ) { + updateCurrentDelayDebounce ??= debounce((proxyName) { + if (proxyName != null) { + clashCore.delay( + proxyName, + ); + } + }); + updateCurrentDelayDebounce!([proxyName]); + } + + init({ + required AppState appState, + required Config config, + required ClashConfig clashConfig, + }) async { + appState.isInit = clashCore.isInit; + if (!appState.isInit) { + appState.isInit = await clashService.init( + config: config, + clashConfig: clashConfig, + ); + } + if (!appState.isInit) return; + await updateClashConfig( + clashConfig: clashConfig, + config: config, + isPatch: false, + ); + updateGroups(appState); + updateCoreVersionInfo(appState); + } + + updatePackages(AppState appState) async { + if (appState.packages.isEmpty && updatePackagesLock == false) { + updatePackagesLock = true; + appState.packages = await app?.getPackages() ?? []; + updatePackagesLock = false; + } + } + + updateNavigationItems({ + required AppState appState, + required Config config, + required ClashConfig clashConfig, + }) { + final hasGroups = appState.getCurrentGroups(clashConfig.mode).isNotEmpty; + final hasProfile = config.profiles.isNotEmpty; + appState.navigationItems = navigation.getItems( + openLogs: config.openLogs, + hasProxies: hasGroups && hasProfile, + ); + } + + updateGroups(AppState appState) { + appState.groups = clashCore.getProxiesGroups(); + } + + showMessage({ + required String title, + required InlineSpan message, + Function()? onTab, + }) { + showCommonDialog( + child: Builder( + builder: (context) { + return AlertDialog( + title: Text(title), + content: SizedBox( + width: 300, + child: RichText( + text: TextSpan( + style: Theme.of(context).textTheme.labelLarge, + children: [message]), + ), + ), + actions: [ + TextButton( + onPressed: onTab ?? + () { + Navigator.of(context).pop(); + }, + child: Text(appLocalizations.confirm), + ) + ], + ); + }, + ), + ); + } + + showCommonDialog({ + required Widget child, + }) async { + return await showModal( + context: navigatorKey.currentState!.context, + configuration: const FadeScaleTransitionConfiguration( + barrierColor: Colors.black38, + ), + builder: (_) => child, + filter: appConstant.filter, + ); + } + + checkUpdate(Function()? onTab) async { + final result = await Request.checkForUpdate(); + if (result.type == ResultType.success) { + showMessage( + title: appLocalizations.discovery, + message: TextSpan( + text: result.data, + ), + onTab: onTab, + ); + } + } + + updateTraffic({ + AppState? appState, + required Config config, + }) { + final traffic = clashCore.getTraffic(); + if(appState != null){ + appState.addTraffic(traffic); + } + if (Platform.isAndroid) { + final currentProfile = config.currentProfile; + if (currentProfile == null) return; + proxyManager.startForeground( + title: currentProfile.label ?? currentProfile.id, + content: "$traffic", + ); + } + } +} + +final globalState = GlobalState(); diff --git a/lib/widgets/android_container.dart b/lib/widgets/android_container.dart new file mode 100644 index 0000000..50fe1af --- /dev/null +++ b/lib/widgets/android_container.dart @@ -0,0 +1,45 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AndroidContainer extends StatefulWidget { + final Widget child; + + const AndroidContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _AndroidContainerState(); +} + +class _AndroidContainerState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + WidgetsBinding.instance.addObserver(this); + } + + @override + Future didChangeAppLifecycleState(AppLifecycleState state) async { + final isPaused = state == AppLifecycleState.paused; + if (isPaused) { + await context.appController.savePreferences(); + } + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + +} diff --git a/lib/widgets/animate_grid.dart b/lib/widgets/animate_grid.dart new file mode 100644 index 0000000..7ce7255 --- /dev/null +++ b/lib/widgets/animate_grid.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +typedef AnimatedGridBuilder = Widget Function(BuildContext, T item); + +class AnimateGrid extends StatelessWidget { + final int columns; + final double itemHeight; + final double gap; + final List items; + final Key Function(T item) keyBuilder; + final AnimatedGridBuilder builder; + final Duration duration; + final Curve curve; + + const AnimateGrid({ + super.key, + required this.items, + required this.itemHeight, + required this.keyBuilder, + required this.builder, + this.gap = 8, + this.duration = const Duration(milliseconds: 300), + this.curve = Curves.easeOut, + this.columns = 2, + }); + + int _rows(int columns, int count) => (count / columns).ceil(); + + Offset _getOffset( + int index, + int count, + double itemWidth, + double itemHeight, + ) { + final xIndex = index % columns; + final yIndex = (index / columns).floor(); + return Offset( + xIndex * itemWidth + xIndex * gap, yIndex * itemHeight + yIndex * gap); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, constraints) { + assert(constraints.hasBoundedHeight == false); + final gapWidth = (columns - 1) * gap; + final width = constraints.maxWidth; + final itemWidth = (width - gapWidth) / columns; + final count = items.length; + final rows = _rows(columns, count); + final gapHeight = (rows - 1) * gap; + final height = rows * itemHeight + gapHeight; + return SizedBox( + width: width, + height: height, + child: Stack( + children: [ + for (var i = 0; i <= count - 1; i++) + Builder( + key: keyBuilder(items[i]), + builder: (context) { + final item = items[i]; + final offset = _getOffset( + i, + count, + itemWidth, + itemHeight, + ); + return TweenAnimationBuilder( + tween: Tween(end: offset), + duration: duration, + curve: curve, + builder: (_, offset, child) { + return Transform.translate( + offset: offset, + child: child, + ); + }, + child: SizedBox( + height: itemHeight, + width: itemWidth, + child: builder( + context, + item, + ), + ), + ); + }, + ), + ], + ), + ); + }); + } +} diff --git a/lib/widgets/app_state_container.dart b/lib/widgets/app_state_container.dart new file mode 100644 index 0000000..37f0d60 --- /dev/null +++ b/lib/widgets/app_state_container.dart @@ -0,0 +1,62 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AppStateContainer extends StatelessWidget { + final Widget child; + + const AppStateContainer({ + super.key, + required this.child, + }); + + _autoLaunchContainer(Widget child) { + return Selector( + selector: (_, config) => config.autoLaunch, + builder: (_, isAutoLaunch, child) { + debugPrint("[autoLaunchContainer] update===>"); + autoLaunch?.updateStatus(isAutoLaunch); + return child!; + }, + child: child, + ); + } + + _updateNavigationsContainer(Widget child) { + return Selector3( + selector: (_, appState, config, clashConfig) { + final hasGroups = + appState.getCurrentGroups(clashConfig.mode).isNotEmpty; + final hasProfile = config.profiles.isNotEmpty; + return UpdateNavigationsSelector( + openLogs: config.openLogs, + hasProxies: hasGroups && hasProfile, + ); + }, + builder: (context, state, child) { + debugPrint("[NavigationsContainer] update===>"); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + context.appController.appState.navigationItems = + navigation.getItems( + openLogs: state.openLogs, + hasProxies: state.hasProxies, + ); + }, + ); + return child!; + }, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return _autoLaunchContainer( + _updateNavigationsContainer( + child, + ), + ); + } +} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart new file mode 100644 index 0000000..b4773ab --- /dev/null +++ b/lib/widgets/card.dart @@ -0,0 +1,181 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +import 'text.dart'; + +class Info { + final String label; + final IconData iconData; + + const Info({ + required this.label, + required this.iconData, + }); +} + +class InfoHeader extends StatelessWidget { + final Info info; + + const InfoHeader({ + super.key, + required this.info, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + info.iconData, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + width: 8, + ), + Flexible( + child: TooltipText( + text: Text( + info.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + ], + ), + ); + } +} + +class CommonCard extends StatelessWidget { + const CommonCard({ + super.key, + bool? isSelected, + this.onPressed, + this.info, + this.selectWidget, + required this.child, + }) : isSelected = isSelected ?? false; + + final bool isSelected; + final void Function()? onPressed; + final Widget? selectWidget; + final Widget child; + final Info? info; + + BorderSide getBorderSide(BuildContext context, Set states) { + final colorScheme = Theme.of(context).colorScheme; + var hoverColor = isSelected + ? colorScheme.primary.toLight() + : colorScheme.primary.toLighter(); + if (states.contains(MaterialState.hovered) || + states.contains(MaterialState.focused) || + states.contains(MaterialState.pressed)) { + return BorderSide( + color: hoverColor, + ); + } + return BorderSide( + color: + isSelected ? colorScheme.primary : colorScheme.onBackground.toSoft(), + ); + } + + Color? getBackgroundColor(BuildContext context, Set states) { + final colorScheme = Theme.of(context).colorScheme; + if (isSelected) { + return colorScheme.secondaryContainer; + } + if (states.isEmpty) { + return colorScheme.secondaryContainer.toLittle(); + } + return Theme.of(context) + .outlinedButtonTheme + .style + ?.backgroundColor + ?.resolve(states); + } + + @override + Widget build(BuildContext context) { + var childWidget = child; + + if (info != null) { + childWidget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + flex: 0, + child: InfoHeader( + info: info!, + ), + ), + Flexible( + child: child, + ), + ], + ); + } + + return OutlinedButton( + clipBehavior: Clip.antiAlias, + style: ButtonStyle( + padding: const MaterialStatePropertyAll(EdgeInsets.zero), + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + backgroundColor: MaterialStateProperty.resolveWith( + (states) => getBackgroundColor(context, states), + ), + side: MaterialStateProperty.resolveWith( + (states) => getBorderSide(context, states), + ), + ), + onPressed: () { + if (onPressed != null) { + onPressed!(); + } + }, + child: Builder( + builder: (_) { + List children = []; + children.add(childWidget); + if (selectWidget != null && isSelected) { + children.add( + Positioned.fill( + child: selectWidget!, + ), + ); + } + return Stack( + children: children, + ); + }, + ), + ); + } +} + +class SelectIcon extends StatelessWidget { + const SelectIcon({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.inversePrimary, + shape: const CircleBorder(), + child: Container( + padding: const EdgeInsets.all(4), + child: const Icon( + Icons.check, + size: 16, + ), + ), + ); + } +} diff --git a/lib/widgets/chip.dart b/lib/widgets/chip.dart new file mode 100644 index 0000000..607acd9 --- /dev/null +++ b/lib/widgets/chip.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class CommonChip extends StatelessWidget { + final String label; + + const CommonChip({ + super.key, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Chip( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 4, + ), + side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), + labelStyle: Theme.of(context).textTheme.bodyMedium, + label: Text(label), + ); + } +} diff --git a/lib/widgets/clash_message_container.dart b/lib/widgets/clash_message_container.dart new file mode 100644 index 0000000..fb45a53 --- /dev/null +++ b/lib/widgets/clash_message_container.dart @@ -0,0 +1,64 @@ +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:fl_clash/plugins/app.dart'; + +class ClashMessageContainer extends StatefulWidget { + final Widget child; + + const ClashMessageContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _ClashMessageContainerState(); +} + +class _ClashMessageContainerState extends State + with ClashMessageListener { + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void initState() { + super.initState(); + clashMessage.addListener(this); + } + + @override + Future dispose() async { + clashMessage.removeListener(this); + super.dispose(); + } + + @override + void onDelay(Delay delay) { + context.appController.setDelay(delay); + super.onDelay(delay); + } + + @override + void onLog(Log log) { + debugPrint("$log"); + context.appController.appState.addLog(log); + super.onLog(log); + } + + @override + void onTun(String fd) { + proxyManager.setProtect(int.parse(fd)); + super.onTun(fd); + } + + @override + void onProcess(Metadata metadata) async { + var packageName = await app?.getPackageName(metadata); + globalState.packageNameMap[metadata.uid] = packageName; + super.onProcess(metadata); + } +} diff --git a/lib/widgets/color_scheme_box.dart b/lib/widgets/color_scheme_box.dart new file mode 100644 index 0000000..4ee8856 --- /dev/null +++ b/lib/widgets/color_scheme_box.dart @@ -0,0 +1,99 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; +import 'card.dart'; +import 'grid.dart'; + +class ColorSchemeBox extends StatelessWidget { + final Color? primaryColor; + final bool? isSelected; + final void Function()? onPressed; + + const ColorSchemeBox({ + super.key, + this.primaryColor, + this.onPressed, + this.isSelected, + }); + + ThemeData _getTheme(BuildContext context) { + if (primaryColor != null) { + return Theme.of(context).copyWith( + colorScheme: ColorScheme.fromSeed( + seedColor: primaryColor!, + brightness: Theme.of(context).brightness, + ), + ); + } else { + return Theme.of(context).copyWith( + colorScheme: context.appController.appState.systemColorSchemes + .getSystemColorSchemeForBrightness(Theme.of(context).brightness), + ); + } + } + + @override + Widget build(BuildContext context) { + return Theme( + data: _getTheme(context), + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Stack( + children: [ + CommonCard( + isSelected: isSelected, + onPressed: onPressed, + selectWidget: Container( + alignment: Alignment.center, + child: const SelectIcon(), + ), + child: Container( + padding: const EdgeInsets.all(8), + child: ClipRRect( + borderRadius: BorderRadius.circular(36), + child: SizedBox( + width: 72, + height: 72, + child: Grid( + crossAxisCount: 2, + children: [ + GridItem( + mainAxisCellCount: 2, + child: Container( + color: colorScheme.primary, + ), + ), + GridItem( + mainAxisCellCount: 1, + child: Container( + color: colorScheme.secondary, + ), + ), + GridItem( + mainAxisCellCount: 1, + child: Container( + color: colorScheme.tertiary, + ), + ) + ], + ), + ), + ), + ), + ), + if (primaryColor == null) + const Positioned( + bottom: 4, + right: 4, + child: Icon( + Icons.colorize, + size: 20, + ), + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/widgets/disabled_mask.dart b/lib/widgets/disabled_mask.dart new file mode 100644 index 0000000..3b78251 --- /dev/null +++ b/lib/widgets/disabled_mask.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class DisabledMask extends StatefulWidget { + final Widget child; + final bool status; + + const DisabledMask({ + super.key, + required this.child, + this.status = true, + }); + + @override + State createState() => _DisabledMaskState(); +} + +class _DisabledMaskState extends State { + GlobalKey childKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final child = Container( + key: childKey, + child: widget.child, + ); + if (!widget.status) { + return child; + } + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 0.2126, + 0.7152, + 0.0722, + 0, + 30, + 0.2126, + 0.7152, + 0.0722, + 0, + 30, + 0.2126, + 0.7152, + 0.0722, + 0, + 30, + 0, + 0, + 0, + 1, + 0, + ]), + child: child, + ); + } +} diff --git a/lib/widgets/extend_page.dart b/lib/widgets/extend_page.dart new file mode 100644 index 0000000..5e4d7a2 --- /dev/null +++ b/lib/widgets/extend_page.dart @@ -0,0 +1,72 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:flutter/material.dart'; +import 'side_sheet.dart'; + +showExtendPage( + BuildContext context, { + required Widget body, + required String title, + double? extendPageWidth, + Widget? action, +}) { + final NavigatorState navigator = Navigator.of(context); + final globalKey = GlobalKey(); + final uniqueBody = Container( + key: globalKey, + child: body, + ); + + // Flexible( + // flex: 0, + // child: Row( + // children: [ + // Expanded( + // child: Padding( + // padding: kTabLabelPadding, + // child: Text( + // title, + // style: Theme.of(context).textTheme.titleMedium, + // ), + // ), + // ), + // const SizedBox( + // height: kToolbarHeight, + // width: kToolbarHeight, + // child: CloseButton(), + // ), + // ], + // ), + // ) + navigator.push( + ModalSideSheetRoute( + modalBarrierColor: Colors.black38, + builder: (context) => LayoutBuilder( + builder: (_, __) { + final isMobile = context.isMobile; + final commonScaffold = CommonScaffold( + automaticallyImplyLeading: isMobile ? true : false, + actions: isMobile + ? null + : [ + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ), + ], + title: Text(title), + body: uniqueBody, + ); + return AnimatedContainer( + duration: kThemeAnimationDuration, + width: isMobile ? context.width : extendPageWidth ?? 300, + child: commonScaffold, + ); + }, + ), + constraints: const BoxConstraints(), + filter: appConstant.filter, + ), + ); +} diff --git a/lib/widgets/fade_box.dart b/lib/widgets/fade_box.dart new file mode 100644 index 0000000..1e72799 --- /dev/null +++ b/lib/widgets/fade_box.dart @@ -0,0 +1,59 @@ +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; + +class FadeBox extends StatelessWidget { + final Widget child; + + const FadeBox({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return PageTransitionSwitcher( + transitionBuilder: ( + child, + animation, + secondaryAnimation, + ) { + return Container( + alignment: Alignment.centerLeft, + child: FadeThroughTransition( + animation: animation, + fillColor: Colors.transparent, + secondaryAnimation: secondaryAnimation, + child: child, + ), + ); + }, + child: child, + ); + } +} + +class FadeScaleBox extends StatelessWidget { + final Widget child; + + const FadeScaleBox({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return PageTransitionSwitcher( + transitionBuilder: ( + child, + animation, + secondaryAnimation, + ) { + return FadeScaleTransition( + animation: animation, + child: child, + ); + }, + child: child, + ); + } +} diff --git a/lib/widgets/float_layout.dart b/lib/widgets/float_layout.dart new file mode 100644 index 0000000..a2d8a38 --- /dev/null +++ b/lib/widgets/float_layout.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class FloatLayout extends StatelessWidget { + final Widget floatingWidget; + + final Widget child; + + const FloatLayout({ + super.key, + required this.floatingWidget, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.loose, + children: [ + Center( + child: child, + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + child: floatingWidget, + ), + ), + ], + ); + } +} + +class FloatWrapper extends StatelessWidget { + final Widget child; + + const FloatWrapper({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(kFloatingActionButtonMargin), + child: child, + ); + } +} diff --git a/lib/widgets/grid.dart b/lib/widgets/grid.dart new file mode 100644 index 0000000..4db448f --- /dev/null +++ b/lib/widgets/grid.dart @@ -0,0 +1,417 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'dart:math' as math; + +class Grid extends MultiChildRenderObjectWidget { + final double mainAxisSpacing; + + final double crossAxisSpacing; + + final double? mainAxisExtent; + + final int crossAxisCount; + + final AxisDirection axisDirection; + + final TextDirection textDirection; + + const Grid({ + super.key, + this.mainAxisSpacing = 0, + this.crossAxisSpacing = 0, + int? crossAxisCount, + AxisDirection? axisDirection, + TextDirection? textDirection, + this.mainAxisExtent, + List? children, + }) : crossAxisCount = crossAxisCount ?? 1, + axisDirection = axisDirection ?? AxisDirection.down, + textDirection = textDirection ?? TextDirection.ltr, + super(children: children ?? const []); + + const Grid.baseGap({ + Key? key, + double mainAxisSpacing = 8.0, + double crossAxisSpacing = 8.0, + int? crossAxisCount, + AxisDirection? axisDirection, + TextDirection? textDirection, + double? mainAxisExtent, + List? children, + }) : this( + key: key, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + crossAxisCount: crossAxisCount, + axisDirection: axisDirection, + textDirection: textDirection, + mainAxisExtent: mainAxisExtent, + children: children, + ); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderGrid( + textDirection: textDirection, + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + axisDirection: axisDirection, + mainAxisExtent: mainAxisExtent, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderGrid renderObject, + ) { + renderObject + ..mainAxisSpacing = mainAxisSpacing + ..mainAxisExtent = mainAxisExtent + ..crossAxisSpacing = crossAxisSpacing + ..textDirection = textDirection + ..axisDirection = axisDirection + ..crossAxisCount = crossAxisCount; + } +} + +class RenderGrid extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderGrid({ + required double mainAxisSpacing, + required double crossAxisSpacing, + required int crossAxisCount, + required AxisDirection axisDirection, + required TextDirection textDirection, + double? mainAxisExtent, + }) : _crossAxisCount = crossAxisCount, + _crossAxisSpacing = crossAxisSpacing, + _mainAxisSpacing = mainAxisSpacing, + _axisDirection = axisDirection, + _textDirection = textDirection, + _mainAxisExtent = mainAxisExtent; + + int _crossAxisCount; + + int get crossAxisCount => _crossAxisCount; + + set crossAxisCount(int value) { + if (_crossAxisCount != value) { + _crossAxisCount = value; + markNeedsLayout(); + } + } + + double? _mainAxisExtent; + + double? get mainAxisExtent => _mainAxisExtent; + + set mainAxisExtent(double? value) { + if (_mainAxisExtent != value) { + _mainAxisExtent = value; + markNeedsLayout(); + } + } + + double _mainAxisSpacing; + + double get mainAxisSpacing => _mainAxisSpacing; + + set mainAxisSpacing(double value) { + if (_mainAxisSpacing != value) { + _mainAxisSpacing = value; + markNeedsLayout(); + } + } + + double _crossAxisSpacing; + + double get crossAxisSpacing => _crossAxisSpacing; + + set crossAxisSpacing(double value) { + if (_crossAxisSpacing != value) { + _crossAxisSpacing = value; + markNeedsLayout(); + } + } + + AxisDirection _axisDirection; + + AxisDirection get axisDirection => _axisDirection; + + set axisDirection(AxisDirection value) { + if (_axisDirection != value) { + _axisDirection = value; + markNeedsLayout(); + } + } + + TextDirection _textDirection; + + TextDirection get textDirection => _textDirection; + + set textDirection(TextDirection value) { + if (_textDirection != value) { + _textDirection = value; + markNeedsLayout(); + } + } + + Axis get mainAxis => axisDirectionToAxis(_axisDirection); + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! GridParentData) { + child.parentData = GridParentData(); + } + } + + @override + void performLayout() { + final requestedSize = _computeSize(constraints: constraints); + size = constraints.constrain(requestedSize); + _hasOverflow = size != requestedSize; + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (_hasOverflow) { + context.pushClipRect( + needsCompositing, + offset, + Offset.zero & size, + defaultPaint, + ); + } else { + defaultPaint(context, offset); + } + } + + GridParentData _getParentData(RenderBox child) { + return child.parentData as GridParentData; + } + + void _layoutChild( + RenderBox child, + BoxConstraints constraints, { + bool parentUsesSize = false, + }) { + child.layout(constraints, parentUsesSize: parentUsesSize); + } + + int _computeCrossAxisCellCount( + GridParentData childParentData, + int crossAxisCount, + ) { + return math.min( + childParentData.crossAxisCellCount ?? 1, + crossAxisCount, + ); + } + + Size _computeSize({ + required BoxConstraints constraints, + }) { + final crossAxisExtent = mainAxis == Axis.vertical + ? constraints.maxWidth + : constraints.maxHeight; + final stride = (crossAxisExtent + crossAxisSpacing) / crossAxisCount; + final offsets = List.filled(crossAxisCount, 0.0); + RenderBox? child = firstChild; + while (child != null) { + final childParentData = _getParentData(child); + final crossAxisCellCount = _computeCrossAxisCellCount( + childParentData, + crossAxisCount, + ); + final crossAxisExtent = stride * crossAxisCellCount - crossAxisSpacing; + final shouldFitContent = childParentData.mainAxisCellCount == null; + + double mainAxisExtent = 0; + + if (shouldFitContent) { + final childConstraints = mainAxis == Axis.vertical + ? BoxConstraints.tightFor(width: crossAxisExtent) + : BoxConstraints.tightFor(height: crossAxisExtent); + _layoutChild(child, childConstraints, parentUsesSize: true); + mainAxisExtent = + mainAxis == Axis.vertical ? child.size.height : child.size.width; + } else { + final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1; + mainAxisExtent = (this.mainAxisExtent ?? stride) * mainAxisCellCount - + mainAxisSpacing; + childParentData.realMainAxisExtent = mainAxisExtent; + final childSize = mainAxis == Axis.vertical + ? Size(crossAxisExtent, mainAxisExtent) + : Size(mainAxisExtent, crossAxisExtent); + final childConstraints = BoxConstraints.tight(childSize); + _layoutChild(child, childConstraints); + } + final origin = _getOrigin(offsets, crossAxisCellCount); + final mainAxisOffset = origin.mainAxisOffset; + final crossAxisOffset = origin.crossAxisIndex * stride; + final offset = mainAxis == Axis.vertical + ? Offset(crossAxisOffset, mainAxisOffset) + : Offset(mainAxisOffset, crossAxisOffset); + childParentData.offset = offset; + + final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing; + + for (int i = 0; i < crossAxisCellCount; i++) { + offsets[origin.crossAxisIndex + i] = nextOffset; + } + child = childAfter(child); + } + final mainAxisExtent = offsets.reduce(math.max) - mainAxisSpacing; + + if (axisDirectionIsReversed(axisDirection)) { + child = firstChild; + while (child != null) { + final childParentData = _getParentData(child); + final offset = childParentData.offset; + final crossAxisOffset = offset.getCrossAxisOffset(mainAxis); + final mainAxisOffset = mainAxisExtent - + offset.getMainAxisOffset(mainAxis) - + childParentData.realMainAxisExtent!; + final newOffset = mainAxis == Axis.vertical + ? Offset(crossAxisOffset, mainAxisOffset) + : Offset(mainAxisOffset, crossAxisOffset); + childParentData.offset = newOffset; + child = childAfter(child); + } + } + + if (mainAxis == Axis.vertical && textDirection == TextDirection.rtl) { + child = firstChild; + while (child != null) { + final childParentData = _getParentData(child); + final crossAxisCellCount = crossAxisCount; + final crossAxisCellExtent = + stride * crossAxisCellCount - crossAxisSpacing; + final offset = childParentData.offset; + final crossAxisOffset = + crossAxisExtent - offset.dx - crossAxisCellExtent; + final mainAxisOffset = offset.dy; + final newOffset = Offset(crossAxisOffset, mainAxisOffset); + childParentData.offset = newOffset; + child = childAfter(child); + } + } + + return mainAxis == Axis.vertical + ? Size(crossAxisExtent, mainAxisExtent) + : Size(mainAxisExtent, crossAxisExtent); + } + + bool _hasOverflow = false; +} + +class GridParentData extends ContainerBoxParentData { + int? crossAxisCellCount; + num? mainAxisCellCount; + double? realMainAxisExtent; + + @override + String toString() => + 'crossAxisCellCount=$crossAxisCellCount; mainAxisCellCount=$mainAxisCellCount;'; +} + +class GridItem extends ParentDataWidget { + final int crossAxisCellCount; + final num? mainAxisCellCount; + + const GridItem({ + super.key, + required super.child, + this.mainAxisCellCount, + this.crossAxisCellCount = 1, + }); + + @override + void applyParentData(RenderObject renderObject) { + final parentData = renderObject.parentData; + if (parentData is GridParentData) { + bool needsLayout = false; + if (parentData.crossAxisCellCount != crossAxisCellCount) { + parentData.crossAxisCellCount = crossAxisCellCount; + needsLayout = true; + } + + if (parentData.mainAxisCellCount != mainAxisCellCount) { + parentData.mainAxisCellCount = mainAxisCellCount; + needsLayout = true; + } + + if (needsLayout) { + final targetParent = renderObject.parent; + if (targetParent is RenderGrid) { + targetParent.markNeedsLayout(); + } + } + } + } + + @override + Type get debugTypicalAncestorWidgetClass => GridItem; +} + +class _Origin { + final int crossAxisIndex; + final double mainAxisOffset; + + const _Origin(this.crossAxisIndex, this.mainAxisOffset); +} + +_Origin _getOrigin(List offsets, int crossAxisCount) { + var length = offsets.length; + var origin = const _Origin(0, double.infinity); + for (int i = 0; i < length; i++) { + final offset = offsets[i]; + if (offset.lessOrEqual(origin.mainAxisOffset)) { + continue; + } + int start = 0; + int span = 0; + for (int j = 0; + span < crossAxisCount && + j < length && + length - j >= crossAxisCount - span; + j++) { + if (offset.lessOrEqual(offsets[j])) { + span++; + if (span == crossAxisCount) { + origin = _Origin(start, offset); + } + } else { + start = j + 1; + span = 0; + } + } + } + return origin; +} + +extension on double { + lessOrEqual(double value) { + return value < this || (value - this).abs() < precisionErrorTolerance + 1; + } +} + +extension on Offset { + double getCrossAxisOffset(Axis direction) { + return direction == Axis.vertical ? dx : dy; + } + + double getMainAxisOffset(Axis direction) { + return direction == Axis.vertical ? dy : dx; + } +} diff --git a/lib/widgets/keep_container.dart b/lib/widgets/keep_container.dart new file mode 100644 index 0000000..d5e3e81 --- /dev/null +++ b/lib/widgets/keep_container.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class KeepContainer extends StatefulWidget { + final Widget child; + + const KeepContainer({super.key, required this.child}); + + @override + State createState() => _KeepContainerState(); +} + +class _KeepContainerState extends State + with AutomaticKeepAliveClientMixin { + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/line_chart.dart b/lib/widgets/line_chart.dart new file mode 100644 index 0000000..82956f8 --- /dev/null +++ b/lib/widgets/line_chart.dart @@ -0,0 +1,203 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class Point { + final double x; + final double y; + + const Point(this.x, this.y); + + @override + String toString() { + return 'Point{x: $x, y: $y}'; + } +} + +class LineChart extends StatefulWidget { + final List points; + final Color color; + final double height; + final Duration duration; + + const LineChart({ + super.key, + required this.points, + required this.color, + this.duration = const Duration(milliseconds: 0), + required this.height, + }); + + @override + State createState() => _LineChartState(); +} + +typedef ComputedPath = Path Function(Size size); + +class _LineChartState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + double progress = 0; + List prevPoints = []; + List nextPoints = []; + List points = []; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + points = widget.points; + prevPoints = points; + } + + @override + void didUpdateWidget(LineChart oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.points != points) { + prevPoints = points; + if (!_controller.isCompleted) { + prevPoints = nextPoints; + } + points = widget.points; + _controller.forward(from: 0); + } + } + + List getRenderPoints(List points) { + if (points.isEmpty) return []; + double maxX = points[0].x; + double minX = points[0].x; + double maxY = points[0].y; + double minY = points[0].y; + for (var point in points) { + if (point.x > maxX) maxX = point.x; + if (point.x < minX) minX = point.x; + if (point.y > maxY) maxY = point.y; + if (point.y < minY) minY = point.y; + } + return points.map((e) { + var x = (e.x - minX) / (maxX - minX); + if (x.isNaN) { + x = 0.5; + } + + var y = (e.y - minY) / (maxY - minY); + if (y.isNaN) { + y = 0.5; + } + + return Point( + x, + y, + ); + }).toList(); + } + + List getInterpolatePoints( + List prevPoints, + List points, + double t, + ) { + var renderPrevPoints = getRenderPoints(prevPoints); + var renderPotions = getRenderPoints(points); + return List.generate(renderPotions.length, (i) { + if (i > renderPrevPoints.length - 1) { + return renderPotions[i]; + } + var x = lerpDouble(renderPrevPoints[i].x, renderPotions[i].x, t)!; + var y = lerpDouble(renderPrevPoints[i].y, renderPotions[i].y, t)!; + return Point( + x, + y, + ); + }); + } + + Path getPath(List points, Size size) { + final path = Path() + ..moveTo(points[0].x * size.width, (1 - points[0].y) * size.height); + for (var i = 1; i < points.length; i++) { + path.lineTo(points[i].x * size.width, (1 - points[i].y) * size.height); + } + return path; + } + + ComputedPath getComputedPath({ + required List prevPoints, + required List points, + required progress, + }) { + nextPoints = getInterpolatePoints(prevPoints, points, progress); + Path setSize(Size size) { + final prevPath = getPath(prevPoints, size); + final nextPath = getPath(nextPoints, size); + final prevMetric = prevPath.computeMetrics().first; + final nextMetric = nextPath.computeMetrics().first; + final prevLength = prevMetric.length; + final nextLength = nextMetric.length; + return nextMetric.extractPath( + 0, + prevLength + (nextLength - prevLength) * progress, + ); + } + + return setSize; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller.view, + builder: (_, __) { + return CustomPaint( + painter: LineChartPainter( + color: widget.color, + computedPath: getComputedPath( + prevPoints: prevPoints, + points: points, + progress: _controller.value, + ), + ), + child: SizedBox( + height: widget.height, + width: double.infinity, + ), + ); + }); + } +} + +class LineChartPainter extends CustomPainter { + final ComputedPath computedPath; + final Color color; + + LineChartPainter({ + required this.computedPath, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + canvas.drawPath(computedPath(size), paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart new file mode 100644 index 0000000..55c5c0a --- /dev/null +++ b/lib/widgets/list.dart @@ -0,0 +1,298 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/open_container.dart'; +import 'package:flutter/material.dart'; + +import 'extend_page.dart'; +import 'scaffold.dart'; + +class Delegate { + const Delegate(); +} + +class RadioDelegate extends Delegate { + final T value; + final T groupValue; + final void Function(T?)? onChanged; + + const RadioDelegate({ + required this.value, + required this.groupValue, + this.onChanged, + }); +} + +class SwitchDelegate extends Delegate { + final bool value; + final ValueChanged? onChanged; + + const SwitchDelegate({ + required this.value, + this.onChanged, + }); +} + +class CheckboxDelegate extends Delegate { + final bool value; + final ValueChanged? onChanged; + + const CheckboxDelegate({ + this.value = false, + this.onChanged, + }); +} + +class OpenDelegate extends Delegate { + final Widget widget; + final String title; + final double? extendPageWidth; + + const OpenDelegate({ + required this.title, + required this.widget, + this.extendPageWidth, + }); +} + +class NextDelegate extends Delegate { + final Widget widget; + final String title; + + const NextDelegate({ + required this.title, + required this.widget, + }); +} + +class ListItem extends StatelessWidget { + final Widget? leading; + final Widget title; + final Widget? subtitle; + final EdgeInsets padding; + final bool? prue; + final Widget? trailing; + final Delegate delegate; + final double? horizontalTitleGap; + final void Function()? onTab; + + const ListItem({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + this.horizontalTitleGap, + this.prue, + this.onTab, + }) : delegate = const Delegate(); + + const ListItem.open({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + required OpenDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + }) : onTab = null; + + const ListItem.next({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + required NextDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + }) : onTab = null; + + const ListItem.checkbox({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.only(left: 16, right: 8), + required CheckboxDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + }) : trailing = null, + onTab = null; + + const ListItem.switchItem({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.only(left: 16, right: 8), + required SwitchDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + }) : trailing = null, + onTab = null; + + const ListItem.radio({ + super.key, + required this.title, + this.subtitle, + this.trailing, + this.padding = const EdgeInsets.only(left: 12, right: 16), + required RadioDelegate this.delegate, + this.horizontalTitleGap = 8, + this.prue, + }) : leading = null, + onTab = null; + + _buildListTile({ + void Function()? onTab, + Widget? trailing, + Widget? leading, + }) { + if (prue == true) { + final List children = []; + if (leading != null) { + children.add(leading); + children.add( + SizedBox( + width: horizontalTitleGap, + ), + ); + } + children.add( + Expanded( + child: title, + ), + ); + if (trailing != null) { + children.add( + SizedBox( + width: horizontalTitleGap, + ), + ); + children.add(trailing); + } + return InkWell( + splashFactory: NoSplash.splashFactory, + onTap: onTab, + child: Container( + padding: padding, + child: Row( + children: children, + ), + ), + ); + } + return ListTile( + leading: leading ?? this.leading, + horizontalTitleGap: horizontalTitleGap, + title: title, + subtitle: subtitle, + onTap: onTab, + trailing: trailing ?? this.trailing, + contentPadding: padding, + ); + } + + @override + Widget build(BuildContext context) { + if (delegate is OpenDelegate) { + final openDelegate = delegate as OpenDelegate; + return OpenContainer( + closedBuilder: (_, action) { + openAction() { + final isMobile = context.isMobile; + if (!isMobile) { + showExtendPage( + context, + body: openDelegate.widget, + title: openDelegate.title, + extendPageWidth: openDelegate.extendPageWidth, + ); + return; + } + action(); + } + + return _buildListTile(onTab: openAction); + }, + openBuilder: (_, action) { + return CommonScaffold.open( + onBack: action, + title: Text(openDelegate.title), + body: openDelegate.widget, + ); + }, + ); + } + if (delegate is NextDelegate) { + final nextDelegate = delegate as NextDelegate; + return _buildListTile( + onTab: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CommonScaffold( + body: nextDelegate.widget, + title: Text(nextDelegate.title), + ), + ), + ); + }, + ); + } + if (delegate is CheckboxDelegate) { + final checkboxDelegate = delegate as CheckboxDelegate; + return _buildListTile( + onTab: () { + if (checkboxDelegate.onChanged != null) { + checkboxDelegate.onChanged!(!checkboxDelegate.value); + } + }, + trailing: Checkbox( + value: checkboxDelegate.value, + onChanged: checkboxDelegate.onChanged, + ), + ); + } + if (delegate is SwitchDelegate) { + final switchDelegate = delegate as SwitchDelegate; + return _buildListTile( + onTab: () { + if (switchDelegate.onChanged != null) { + switchDelegate.onChanged!(!switchDelegate.value); + } + }, + trailing: Switch( + value: switchDelegate.value, + onChanged: switchDelegate.onChanged, + ), + ); + } + if (delegate is RadioDelegate) { + final radioDelegate = delegate as RadioDelegate; + return _buildListTile( + onTab: () { + if (radioDelegate.onChanged != null) { + radioDelegate.onChanged!(radioDelegate.value); + } + }, + leading: SizedBox( + width: 32, + height: 32, + child: Radio( + value: radioDelegate.value, + groupValue: radioDelegate.groupValue, + onChanged: radioDelegate.onChanged, + ), + ), + trailing: trailing, + ); + } + + return _buildListTile( + onTab: onTab, + ); + } +} diff --git a/lib/widgets/null_status.dart b/lib/widgets/null_status.dart new file mode 100644 index 0000000..4c4831a --- /dev/null +++ b/lib/widgets/null_status.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import '../common/common.dart'; + +class NullStatus extends StatelessWidget { + final String label; + + const NullStatus({super.key, required this.label}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + label, + style: Theme.of(context).textTheme.titleMedium?.toBold(), + ), + ); + } +} diff --git a/lib/widgets/open_container.dart b/lib/widgets/open_container.dart new file mode 100644 index 0000000..9fc3710 --- /dev/null +++ b/lib/widgets/open_container.dart @@ -0,0 +1,628 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; + +import '../models/models.dart'; + +typedef CloseContainerActionCallback = void Function({S? returnValue}); +typedef OpenContainerBuilder = Widget Function( + BuildContext context, + CloseContainerActionCallback action, +); +typedef CloseContainerBuilder = Widget Function( + BuildContext context, + VoidCallback action, +); + +enum ContainerTransitionType { + fade, + fadeThrough, +} + +typedef ClosedCallback = void Function(S data); + +@optionalTypeArgs +class OpenContainer extends StatefulWidget { + const OpenContainer({ + super.key, + this.middleColor, + this.onClosed, + required this.closedBuilder, + required this.openBuilder, + this.tappable = true, + this.transitionDuration = const Duration(milliseconds: 300), + this.transitionType = ContainerTransitionType.fade, + this.useRootNavigator = false, + this.routeSettings, + this.clipBehavior = Clip.antiAlias, + }); + + final Color? middleColor; + final ClosedCallback? onClosed; + final CloseContainerBuilder closedBuilder; + final OpenContainerBuilder openBuilder; + final bool tappable; + final Duration transitionDuration; + final ContainerTransitionType transitionType; + final bool useRootNavigator; + final RouteSettings? routeSettings; + final Clip clipBehavior; + + @override + State> createState() => _OpenContainerState(); +} + +class _OpenContainerState extends State> { + final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>(); + final GlobalKey _closedBuilderKey = GlobalKey(); + + Future openContainer() async { + final Color middleColor = + widget.middleColor ?? Theme.of(context).canvasColor; + final T? data = await Navigator.of( + context, + rootNavigator: widget.useRootNavigator, + ).push(_OpenContainerRoute( + middleColor: middleColor, + closedBuilder: widget.closedBuilder, + openBuilder: widget.openBuilder, + hideableKey: _hideableKey, + closedBuilderKey: _closedBuilderKey, + transitionDuration: widget.transitionDuration, + transitionType: widget.transitionType, + useRootNavigator: widget.useRootNavigator, + routeSettings: widget.routeSettings, + )); + if (widget.onClosed != null) { + widget.onClosed!(data); + } + } + + @override + Widget build(BuildContext context) { + return _Hideable( + key: _hideableKey, + child: GestureDetector( + onTap: widget.tappable ? openContainer : null, + child: Material( + clipBehavior: widget.clipBehavior, + child: Builder( + key: _closedBuilderKey, + builder: (BuildContext context) { + return widget.closedBuilder(context, openContainer); + }, + ), + ), + ), + ); + } +} + +class _Hideable extends StatefulWidget { + const _Hideable({ + super.key, + required this.child, + }); + + final Widget child; + + @override + State<_Hideable> createState() => _HideableState(); +} + +class _HideableState extends State<_Hideable> { + Size? get placeholderSize => _placeholderSize; + Size? _placeholderSize; + + set placeholderSize(Size? value) { + if (_placeholderSize == value) { + return; + } + setState(() { + _placeholderSize = value; + }); + } + + bool get isVisible => _visible; + bool _visible = true; + + set isVisible(bool value) { + if (_visible == value) { + return; + } + setState(() { + _visible = value; + }); + } + + bool get isInTree => _placeholderSize == null; + + @override + Widget build(BuildContext context) { + if (_placeholderSize != null) { + return SizedBox.fromSize(size: _placeholderSize); + } + return Visibility( + visible: _visible, + maintainSize: true, + maintainState: true, + maintainAnimation: true, + child: widget.child, + ); + } +} + +class _OpenContainerRoute extends ModalRoute { + _OpenContainerRoute({ + required this.middleColor, + required this.closedBuilder, + required this.openBuilder, + required this.hideableKey, + required this.closedBuilderKey, + required this.transitionDuration, + required this.transitionType, + required this.useRootNavigator, + required RouteSettings? routeSettings, + }) : _closedOpacityTween = _getClosedOpacityTween(transitionType), + _openOpacityTween = _getOpenOpacityTween(transitionType), + super(settings: routeSettings); + + static _FlippableTweenSequence _getColorTween({ + required ContainerTransitionType transitionType, + required Color closedColor, + required Color openColor, + required Color middleColor, + }) { + switch (transitionType) { + case ContainerTransitionType.fade: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(closedColor), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ColorTween(begin: closedColor, end: openColor), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ConstantTween(openColor), + weight: 3 / 5, + ), + ], + ); + case ContainerTransitionType.fadeThrough: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: ColorTween(begin: closedColor, end: middleColor), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ColorTween(begin: middleColor, end: openColor), + weight: 4 / 5, + ), + ], + ); + } + } + + static _FlippableTweenSequence _getClosedOpacityTween( + ContainerTransitionType transitionType) { + switch (transitionType) { + case ContainerTransitionType.fade: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(1.0), + weight: 1, + ), + ], + ); + case ContainerTransitionType.fadeThrough: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 0.0), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 4 / 5, + ), + ], + ); + } + } + + static _FlippableTweenSequence _getOpenOpacityTween( + ContainerTransitionType transitionType) { + switch (transitionType) { + case ContainerTransitionType.fade: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ConstantTween(1.0), + weight: 3 / 5, + ), + ], + ); + case ContainerTransitionType.fadeThrough: + return _FlippableTweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0), + weight: 4 / 5, + ), + ], + ); + } + } + + final Color middleColor; + final CloseContainerBuilder closedBuilder; + final OpenContainerBuilder openBuilder; + final GlobalKey<_HideableState> hideableKey; + final GlobalKey closedBuilderKey; + + @override + final Duration transitionDuration; + final ContainerTransitionType transitionType; + + final bool useRootNavigator; + + final _FlippableTweenSequence _closedOpacityTween; + final _FlippableTweenSequence _openOpacityTween; + late _FlippableTweenSequence _colorTween; + + static final TweenSequence _scrimFadeInTween = TweenSequence( + >[ + TweenSequenceItem( + tween: ColorTween(begin: Colors.transparent, end: Colors.black54), + weight: 1 / 5, + ), + TweenSequenceItem( + tween: ConstantTween(Colors.black54), + weight: 4 / 5, + ), + ], + ); + static final Tween _scrimFadeOutTween = ColorTween( + begin: Colors.transparent, + end: Colors.black54, + ); + final GlobalKey _openBuilderKey = GlobalKey(); + final RectTween _rectTween = RectTween(); + + AnimationStatus? _lastAnimationStatus; + AnimationStatus? _currentAnimationStatus; + + @override + TickerFuture didPush() { + _takeMeasurements(navigatorContext: hideableKey.currentContext!); + + animation!.addStatusListener((AnimationStatus status) { + _lastAnimationStatus = _currentAnimationStatus; + _currentAnimationStatus = status; + switch (status) { + case AnimationStatus.dismissed: + _toggleHideable(hide: false); + break; + case AnimationStatus.completed: + _toggleHideable(hide: true); + break; + case AnimationStatus.forward: + case AnimationStatus.reverse: + break; + } + }); + + return super.didPush(); + } + + @override + bool didPop(T? result) { + _takeMeasurements( + navigatorContext: subtreeContext!, + delayForSourceRoute: true, + ); + return super.didPop(result); + } + + @override + void dispose() { + if (hideableKey.currentState?.isVisible == false) { + SchedulerBinding.instance + .addPostFrameCallback((Duration d) => _toggleHideable(hide: false)); + } + super.dispose(); + } + + void _toggleHideable({required bool hide}) { + if (hideableKey.currentState != null) { + hideableKey.currentState! + ..placeholderSize = null + ..isVisible = !hide; + } + } + + void _takeMeasurements({ + required BuildContext navigatorContext, + bool delayForSourceRoute = false, + }) { + final RenderBox navigator = Navigator.of( + navigatorContext, + rootNavigator: useRootNavigator, + ).context.findRenderObject()! as RenderBox; + final Size navSize = _getSize(navigator); + _rectTween.end = Offset.zero & navSize; + + void takeMeasurementsInSourceRoute([Duration? _]) { + if (!navigator.attached || hideableKey.currentContext == null) { + return; + } + _rectTween.begin = _getRect(hideableKey, navigator); + hideableKey.currentState!.placeholderSize = _rectTween.begin!.size; + } + + if (delayForSourceRoute) { + SchedulerBinding.instance + .addPostFrameCallback(takeMeasurementsInSourceRoute); + } else { + takeMeasurementsInSourceRoute(); + } + } + + Size _getSize(RenderBox render) { + assert(render.hasSize); + return render.size; + } + + Rect _getRect(GlobalKey key, RenderBox ancestor) { + assert(key.currentContext != null); + assert(ancestor.hasSize); + final RenderBox render = + key.currentContext!.findRenderObject()! as RenderBox; + assert(render.hasSize); + return MatrixUtils.transformRect( + render.getTransformTo(ancestor), + Offset.zero & render.size, + ); + } + + bool get _transitionWasInterrupted { + bool wasInProgress = false; + bool isInProgress = false; + + switch (_currentAnimationStatus) { + case AnimationStatus.completed: + case AnimationStatus.dismissed: + isInProgress = false; + break; + case AnimationStatus.forward: + case AnimationStatus.reverse: + isInProgress = true; + break; + case null: + break; + } + switch (_lastAnimationStatus) { + case AnimationStatus.completed: + case AnimationStatus.dismissed: + wasInProgress = false; + break; + case AnimationStatus.forward: + case AnimationStatus.reverse: + wasInProgress = true; + break; + case null: + break; + } + return wasInProgress && isInProgress; + } + + void closeContainer({T? returnValue}) { + Navigator.of(subtreeContext!).pop(returnValue); + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Selector( + selector: (_, config) => config.themeMode, + builder: (_, __, ___) { + debugPrint("[OpenContainerTheme] update===>"); + _colorTween = _getColorTween( + transitionType: transitionType, + closedColor: Theme.of(context).colorScheme.background, + openColor: Theme.of(context).colorScheme.background, + middleColor: middleColor, + ); + return Align( + alignment: Alignment.topLeft, + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + if (animation.isCompleted) { + return SizedBox.expand( + child: Material( + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder(context, closeContainer); + }, + ), + ), + ); + } + + final Animation curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + reverseCurve: _transitionWasInterrupted + ? null + : Curves.fastOutSlowIn.flipped, + ); + TweenSequence? colorTween; + TweenSequence? closedOpacityTween, openOpacityTween; + Animatable? scrimTween; + switch (animation.status) { + case AnimationStatus.dismissed: + case AnimationStatus.forward: + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + break; + case AnimationStatus.reverse: + if (_transitionWasInterrupted) { + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + break; + } + closedOpacityTween = _closedOpacityTween.flipped; + openOpacityTween = _openOpacityTween.flipped; + colorTween = _colorTween.flipped; + scrimTween = _scrimFadeOutTween; + break; + case AnimationStatus.completed: + assert(false); // Unreachable. + break; + } + assert(colorTween != null); + assert(closedOpacityTween != null); + assert(openOpacityTween != null); + assert(scrimTween != null); + + final Rect rect = _rectTween.evaluate(curvedAnimation)!; + return SizedBox.expand( + child: Container( + color: scrimTween!.evaluate(curvedAnimation), + child: Align( + alignment: Alignment.topLeft, + child: Transform.translate( + offset: Offset(rect.left, rect.top), + child: SizedBox( + width: rect.width, + height: rect.height, + child: Material( + clipBehavior: Clip.antiAlias, + animationDuration: Duration.zero, + color: colorTween!.evaluate(animation), + child: Stack( + fit: StackFit.passthrough, + children: [ + // Closed child fading out. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.begin!.width, + height: _rectTween.begin!.height, + child: (hideableKey.currentState?.isInTree ?? + false) + ? null + : FadeTransition( + opacity: closedOpacityTween! + .animate(animation), + child: Builder( + key: closedBuilderKey, + builder: (BuildContext context) { + // Use dummy "open container" callback + // since we are in the process of opening. + return closedBuilder( + context, () {}); + }, + ), + ), + ), + ), + + // Open child fading in. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.end!.width, + height: _rectTween.end!.height, + child: FadeTransition( + opacity: + openOpacityTween!.animate(animation), + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder( + context, closeContainer); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ), + ); + }, + ); + } + + @override + bool get maintainState => true; + + @override + Color? get barrierColor => null; + + @override + bool get opaque => true; + + @override + bool get barrierDismissible => false; + + @override + String? get barrierLabel => null; +} + +class _FlippableTweenSequence extends TweenSequence { + _FlippableTweenSequence(this._items) : super(_items); + + final List> _items; + _FlippableTweenSequence? _flipped; + + _FlippableTweenSequence? get flipped { + if (_flipped == null) { + final List> newItems = >[]; + for (int i = 0; i < _items.length; i++) { + newItems.add(TweenSequenceItem( + tween: _items[i].tween, + weight: _items[_items.length - 1 - i].weight, + )); + } + _flipped = _FlippableTweenSequence(newItems); + } + return _flipped; + } +} diff --git a/lib/widgets/pop_container.dart b/lib/widgets/pop_container.dart new file mode 100644 index 0000000..6334a35 --- /dev/null +++ b/lib/widgets/pop_container.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + + +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/widgets.dart'; + +class PopContainer extends StatefulWidget { + final Widget child; + + const PopContainer({super.key, required this.child}); + + @override + State createState() => _PopContainerState(); +} + +class _PopContainerState extends State { + @override + Widget build(BuildContext context) { + if (Platform.isAndroid) { + return PopScope( + canPop: false, + onPopInvoked: (didPop) async { + final canPop = Navigator.canPop(context); + if (canPop) { + Navigator.pop(context); + } else { + await context.appController.handleBackOrExit(); + } + }, + child: widget.child, + ); + } + return widget.child; + } +} diff --git a/lib/widgets/popup_menu.dart b/lib/widgets/popup_menu.dart new file mode 100644 index 0000000..06d63a5 --- /dev/null +++ b/lib/widgets/popup_menu.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; + +class CommonPopupMenuItem { + T action; + String label; + IconData? iconData; + + CommonPopupMenuItem({ + required this.action, + required this.label, + this.iconData, + }); +} + +class CommonPopupMenu extends StatefulWidget { + final List items; + final PopupMenuItemSelected onSelected; + final T? selectedValue; + final Widget? icon; + + const CommonPopupMenu({ + super.key, + required this.items, + required this.onSelected, + this.icon, + }) : selectedValue = null; + + const CommonPopupMenu.radio({ + super.key, + required this.items, + required this.onSelected, + required T this.selectedValue, + this.icon, + }); + + @override + State> createState() => _CommonPopupMenuState(); +} + +class _CommonPopupMenuState extends State> { + late ValueNotifier groupValue; + + @override + void initState() { + super.initState(); + groupValue = ValueNotifier(widget.selectedValue); + } + + handleSelect(T value) { + if (widget.selectedValue != null) { + this.groupValue.value = value; + } + widget.onSelected(value); + } + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: widget.icon, + onSelected: handleSelect, + itemBuilder: (_) { + return [ + for (final item in widget.items) + PopupMenuItem( + value: item.action, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + item.iconData != null + ? Flexible( + child: Container( + margin: const EdgeInsets.only(right: 16), + child: Icon(item.iconData), + ), + ) + : Container(), + Flexible( + flex: 0, + child: SizedBox( + child: Text( + item.label, + maxLines: 1, + ), + ), + ), + ], + ), + ), + if (widget.selectedValue != null) + Flexible( + flex: 0, + child: ValueListenableBuilder( + valueListenable: groupValue, + builder: (_, groupValue, __) { + return Radio( + value: item.action, + groupValue: groupValue, + onChanged: (T? value) { + if (value != null) { + handleSelect(value); + Navigator.of(context).pop(); + } + }, + ); + }, + ), + ), + ], + ), + ), + ]; + }, + ); + } +} diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart new file mode 100644 index 0000000..333c6e8 --- /dev/null +++ b/lib/widgets/scaffold.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CommonScaffold extends StatefulWidget { + final Widget body; + final Widget? bottomNavigationBar; + final Widget? title; + final Widget? leading; + final List? actions; + final bool automaticallyImplyLeading; + + const CommonScaffold({ + super.key, + required this.body, + this.bottomNavigationBar, + this.leading, + this.title, + this.actions, + this.automaticallyImplyLeading = true, + }); + + CommonScaffold.open({ + Key? key, + required Widget body, + Widget? title, + required Function onBack, + }) : this( + key: key, + body: body, + title: title, + automaticallyImplyLeading: false, + leading: SizedBox( + height: kToolbarHeight, + child: IconButton( + icon: const BackButtonIcon(), + onPressed: () { + onBack(); + }, + ), + ), + ); + + @override + State createState() => CommonScaffoldState(); +} + +class CommonScaffoldState extends State { + final ValueNotifier> _actions = ValueNotifier([]); + + final ValueNotifier _loading = ValueNotifier(false); + + set actions(List actions) { + if (_actions.value != actions) { + _actions.value = actions; + } + } + + loadingRun(Future Function() futureFunction) async { + _loading.value = true; + await futureFunction(); + _loading.value = false; + } + + @override + void didUpdateWidget(covariant CommonScaffold oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.title != widget.title) { + _actions.value = []; + } + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + ), + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + ValueListenableBuilder( + valueListenable: _actions, + builder: (_, actions, __) { + return AppBar( + automaticallyImplyLeading: widget.automaticallyImplyLeading, + leading: widget.leading, + title: widget.title, + actions: actions.isNotEmpty ? actions : widget.actions, + ); + }, + ), + ValueListenableBuilder( + valueListenable: _loading, + builder: (_, value, __) { + return value == true + ? const LinearProgressIndicator() + : Container(); + }, + ), + ], + ), + ), + body: widget.body, + bottomNavigationBar: widget.bottomNavigationBar, + ), + ); + } +} diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart new file mode 100644 index 0000000..b745866 --- /dev/null +++ b/lib/widgets/side_sheet.dart @@ -0,0 +1,645 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +const Duration _bottomSheetEnterDuration = Duration(milliseconds: 300); +const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); +const Curve _modalBottomSheetCurve = decelerateEasing; +const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0; + +class SideSheet extends StatefulWidget { + const SideSheet({ + super.key, + this.animationController, + this.enableDrag = true, + this.showDragHandle, + this.dragHandleColor, + this.dragHandleSize, + this.onDragStart, + this.onDragEnd, + this.backgroundColor, + this.shadowColor, + this.elevation, + this.shape, + this.clipBehavior, + this.constraints, + required this.onClosing, + required this.builder, + }) : assert(elevation == null || elevation >= 0.0); + + final AnimationController? animationController; + + final VoidCallback onClosing; + + final WidgetBuilder builder; + + final bool enableDrag; + + final bool? showDragHandle; + + final Color? dragHandleColor; + + final Size? dragHandleSize; + + final BottomSheetDragStartHandler? onDragStart; + + final BottomSheetDragEndHandler? onDragEnd; + + final Color? backgroundColor; + + final Color? shadowColor; + + final double? elevation; + + final ShapeBorder? shape; + + final Clip? clipBehavior; + + final BoxConstraints? constraints; + + @override + State createState() => _SideSheetState(); + + static AnimationController createAnimationController(TickerProvider vsync) { + return AnimationController( + duration: _bottomSheetEnterDuration, + reverseDuration: _bottomSheetExitDuration, + debugLabel: 'SideSheet', + vsync: vsync, + ); + } +} + +class _SideSheetState extends State { + final GlobalKey _childKey = GlobalKey(debugLabel: 'SideSheet child'); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final Color color = widget.backgroundColor ?? colorScheme.surface; + final Color surfaceTintColor = colorScheme.surfaceTint; + final Color shadowColor = widget.shadowColor ?? Colors.transparent; + final double elevation = widget.elevation ?? 0; + final ShapeBorder shape = widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ); + + final BoxConstraints constraints = + widget.constraints ?? const BoxConstraints(maxWidth: 320); + + final Clip clipBehavior = widget.clipBehavior ?? Clip.none; + + Widget sideSheet = Material( + key: _childKey, + color: color, + elevation: elevation, + surfaceTintColor: surfaceTintColor, + shadowColor: shadowColor, + shape: shape, + clipBehavior: clipBehavior, + child: widget.builder(context), + ); + + return ConstrainedBox( + constraints: constraints, + child: sideSheet, + ); + } +} + +typedef _SizeChangeCallback = void Function(Size); + +class _SideSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget { + const _SideSheetLayoutWithSizeListener({ + required this.onChildSizeChanged, + required this.animationValue, + required this.isScrollControlled, + required this.scrollControlDisabledMaxHeightRatio, + super.child, + }); + + final _SizeChangeCallback onChildSizeChanged; + final double animationValue; + final bool isScrollControlled; + final double scrollControlDisabledMaxHeightRatio; + + @override + _RenderSideSheetLayoutWithSizeListener createRenderObject( + BuildContext context) { + return _RenderSideSheetLayoutWithSizeListener( + onChildSizeChanged: onChildSizeChanged, + animationValue: animationValue, + isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, + ); + } + + @override + void updateRenderObject(BuildContext context, + _RenderSideSheetLayoutWithSizeListener renderObject) { + renderObject.onChildSizeChanged = onChildSizeChanged; + renderObject.animationValue = animationValue; + renderObject.isScrollControlled = isScrollControlled; + renderObject.scrollControlDisabledMaxHeightRatio = + scrollControlDisabledMaxHeightRatio; + } +} + +class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox { + _RenderSideSheetLayoutWithSizeListener({ + RenderBox? child, + required _SizeChangeCallback onChildSizeChanged, + required double animationValue, + required bool isScrollControlled, + required double scrollControlDisabledMaxHeightRatio, + }) : _onChildSizeChanged = onChildSizeChanged, + _animationValue = animationValue, + _isScrollControlled = isScrollControlled, + _scrollControlDisabledMaxHeightRatio = + scrollControlDisabledMaxHeightRatio, + super(child); + + Size _lastSize = Size.zero; + + _SizeChangeCallback get onChildSizeChanged => _onChildSizeChanged; + _SizeChangeCallback _onChildSizeChanged; + + set onChildSizeChanged(_SizeChangeCallback newCallback) { + if (_onChildSizeChanged == newCallback) { + return; + } + + _onChildSizeChanged = newCallback; + markNeedsLayout(); + } + + double get animationValue => _animationValue; + double _animationValue; + + set animationValue(double newValue) { + if (_animationValue == newValue) { + return; + } + + _animationValue = newValue; + markNeedsLayout(); + } + + bool get isScrollControlled => _isScrollControlled; + bool _isScrollControlled; + + set isScrollControlled(bool newValue) { + if (_isScrollControlled == newValue) { + return; + } + + _isScrollControlled = newValue; + markNeedsLayout(); + } + + double get scrollControlDisabledMaxHeightRatio => + _scrollControlDisabledMaxHeightRatio; + double _scrollControlDisabledMaxHeightRatio; + + set scrollControlDisabledMaxHeightRatio(double newValue) { + if (_scrollControlDisabledMaxHeightRatio == newValue) { + return; + } + + _scrollControlDisabledMaxHeightRatio = newValue; + markNeedsLayout(); + } + + Size _getSize(BoxConstraints constraints) { + return constraints.constrain(constraints.biggest); + } + + @override + double computeMinIntrinsicWidth(double height) { + final double width = + _getSize(BoxConstraints.tightForFinite(height: height)).width; + if (width.isFinite) { + return width; + } + return 0.0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final double width = + _getSize(BoxConstraints.tightForFinite(height: height)).width; + if (width.isFinite) { + return width; + } + return 0.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + final double height = + _getSize(BoxConstraints.tightForFinite(width: width)).height; + if (height.isFinite) { + return height; + } + return 0.0; + } + + @override + double computeMaxIntrinsicHeight(double width) { + final double height = + _getSize(BoxConstraints.tightForFinite(width: width)).height; + if (height.isFinite) { + return height; + } + return 0.0; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return _getSize(constraints); + } + + BoxConstraints _getConstraintsForChild(BoxConstraints constraints) { + return BoxConstraints( + maxHeight: constraints.maxHeight, + ); + } + + Offset _getPositionForChild(Size size, Size childSize) { + return Offset(size.width - childSize.width * animationValue, 0.0); + } + + @override + void performLayout() { + size = _getSize(constraints); + if (child != null) { + final BoxConstraints childConstraints = + _getConstraintsForChild(constraints); + assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); + child!.layout( + childConstraints, + parentUsesSize: !childConstraints.isTight, + ); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = _getPositionForChild( + size, + childConstraints.isTight ? childConstraints.smallest : child!.size, + ); + final Size childSize = + childConstraints.isTight ? childConstraints.smallest : child!.size; + + if (_lastSize != childSize) { + _lastSize = childSize; + _onChildSizeChanged.call(_lastSize); + } + } + } +} + +class _ModalSideSheet extends StatefulWidget { + const _ModalSideSheet({ + super.key, + required this.route, + this.backgroundColor, + this.elevation, + this.shape, + this.clipBehavior, + this.constraints, + this.isScrollControlled = false, + this.scrollControlDisabledMaxHeightRatio = + _defaultScrollControlDisabledMaxHeightRatio, + this.enableDrag = true, + this.showDragHandle = false, + }); + + final ModalSideSheetRoute route; + final bool isScrollControlled; + final double scrollControlDisabledMaxHeightRatio; + final Color? backgroundColor; + final double? elevation; + final ShapeBorder? shape; + final Clip? clipBehavior; + final BoxConstraints? constraints; + final bool enableDrag; + final bool showDragHandle; + + @override + _ModalSideSheetState createState() => _ModalSideSheetState(); +} + +class _ModalSideSheetState extends State<_ModalSideSheet> { + ParametricCurve animationCurve = _modalBottomSheetCurve; + + String _getRouteLabel(MaterialLocalizations localizations) { + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return ''; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return localizations.dialogLabel; + } + } + + EdgeInsets _getNewClipDetails(Size topLayerSize) { + return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + assert(debugCheckHasMaterialLocalizations(context)); + final MaterialLocalizations localizations = + MaterialLocalizations.of(context); + final String routeLabel = _getRouteLabel(localizations); + + return AnimatedBuilder( + animation: widget.route.animation!, + child: SideSheet( + animationController: widget.route._animationController, + onClosing: () { + if (widget.route.isCurrent) { + Navigator.pop(context); + } + }, + builder: widget.route.builder, + backgroundColor: widget.backgroundColor, + elevation: widget.elevation, + shape: widget.shape, + clipBehavior: widget.clipBehavior, + constraints: widget.constraints, + enableDrag: widget.enableDrag, + showDragHandle: widget.showDragHandle, + ), + builder: (BuildContext context, Widget? child) { + final double animationValue = animationCurve.transform( + widget.route.animation!.value, + ); + return Semantics( + scopesRoute: true, + namesRoute: true, + label: routeLabel, + explicitChildNodes: true, + child: ClipRect( + child: _SideSheetLayoutWithSizeListener( + onChildSizeChanged: (Size size) { + widget.route._didChangeBarrierSemanticsClip( + _getNewClipDetails(size), + ); + }, + animationValue: animationValue, + isScrollControlled: widget.isScrollControlled, + scrollControlDisabledMaxHeightRatio: + widget.scrollControlDisabledMaxHeightRatio, + child: child, + ), + ), + ); + }, + ); + } +} + +class ModalSideSheetRoute extends PopupRoute { + ModalSideSheetRoute({ + required this.builder, + this.capturedThemes, + this.barrierLabel, + this.barrierOnTapHint, + this.backgroundColor, + this.elevation, + this.shape, + this.clipBehavior, + this.constraints, + this.modalBarrierColor, + this.isDismissible = true, + this.isScrollControlled = false, + this.scrollControlDisabledMaxHeightRatio = + _defaultScrollControlDisabledMaxHeightRatio, + super.settings, + this.transitionAnimationController, + this.anchorPoint, + this.useSafeArea = false, + super.filter + }); + + final WidgetBuilder builder; + + final CapturedThemes? capturedThemes; + + final bool isScrollControlled; + + final double scrollControlDisabledMaxHeightRatio; + + final Color? backgroundColor; + + final double? elevation; + + final ShapeBorder? shape; + + final Clip? clipBehavior; + + final BoxConstraints? constraints; + + final Color? modalBarrierColor; + + final bool isDismissible; + + final AnimationController? transitionAnimationController; + + final Offset? anchorPoint; + + final bool useSafeArea; + + final String? barrierOnTapHint; + + final ValueNotifier _clipDetailsNotifier = + ValueNotifier(EdgeInsets.zero); + + @override + void dispose() { + _clipDetailsNotifier.dispose(); + super.dispose(); + } + + bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) { + if (_clipDetailsNotifier.value == newClipDetails) { + return false; + } + _clipDetailsNotifier.value = newClipDetails; + return true; + } + + @override + Duration get transitionDuration => _bottomSheetEnterDuration; + + @override + Duration get reverseTransitionDuration => _bottomSheetExitDuration; + + @override + bool get barrierDismissible => isDismissible; + + @override + final String? barrierLabel; + + @override + Color get barrierColor => modalBarrierColor ?? Colors.black54; + + AnimationController? _animationController; + + @override + AnimationController createAnimationController() { + assert(_animationController == null); + if (transitionAnimationController != null) { + _animationController = transitionAnimationController; + willDisposeAnimationController = false; + } else { + _animationController = SideSheet.createAnimationController(navigator!); + } + return _animationController!; + } + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + final Widget content = DisplayFeatureSubScreen( + anchorPoint: anchorPoint, + child: Builder( + builder: (BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return _ModalSideSheet( + route: this, + backgroundColor: backgroundColor ?? colorScheme.surface, + elevation: elevation ?? 0, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: + scrollControlDisabledMaxHeightRatio, + ); + }, + ), + ); + + final Widget sideSheet = content; + + return capturedThemes?.wrap(sideSheet) ?? sideSheet; + } + + @override + Widget buildModalBarrier() { + if (barrierColor.alpha != 0 && !offstage) { + assert(barrierColor != barrierColor.withOpacity(0.0)); + final Animation color = animation!.drive( + ColorTween( + begin: barrierColor.withOpacity(0.0), + end: barrierColor, + ).chain( + CurveTween(curve: barrierCurve), + ), + ); + return AnimatedModalBarrier( + color: color, + dismissible: barrierDismissible, + semanticsLabel: barrierLabel, + barrierSemanticsDismissible: semanticsDismissible, + clipDetailsNotifier: _clipDetailsNotifier, + semanticsOnTapHint: barrierOnTapHint, + ); + } else { + return ModalBarrier( + dismissible: barrierDismissible, + semanticsLabel: barrierLabel, + barrierSemanticsDismissible: semanticsDismissible, + clipDetailsNotifier: _clipDetailsNotifier, + semanticsOnTapHint: barrierOnTapHint, + ); + } + } +} + +Future showModalSideSheet({ + required BuildContext context, + required Widget body, + required String title, + Color? backgroundColor, + String? barrierLabel, + double? elevation, + ShapeBorder? shape, + Clip? clipBehavior, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = false, + double scrollControlDisabledMaxHeightRatio = + _defaultScrollControlDisabledMaxHeightRatio, + bool useRootNavigator = false, + bool isDismissible = true, + bool useSafeArea = false, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Offset? anchorPoint, +}) { + assert(debugCheckHasMediaQuery(context)); + assert(debugCheckHasMaterialLocalizations(context)); + + final NavigatorState navigator = + Navigator.of(context, rootNavigator: useRootNavigator); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + return navigator.push(ModalSideSheetRoute( + builder: (context) { + return Column( + children: [ + Flexible( + flex: 0, + child: Row( + children: [ + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: BackButton(), + ), + const SizedBox(width: 8,), + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ), + ], + ), + ), + Expanded( + child: body, + ), + ], + ); + }, + capturedThemes: + InheritedTheme.capture(from: context, to: navigator.context), + isScrollControlled: isScrollControlled, + scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, + barrierLabel: barrierLabel ?? localizations.scrimLabel, + barrierOnTapHint: + localizations.scrimOnTapHint(localizations.bottomSheetLabel), + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + isDismissible: isDismissible, + modalBarrierColor: + barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor, + settings: routeSettings, + transitionAnimationController: transitionAnimationController, + anchorPoint: anchorPoint, + useSafeArea: useSafeArea, + )); +} diff --git a/lib/widgets/text.dart b/lib/widgets/text.dart new file mode 100644 index 0000000..c68a739 --- /dev/null +++ b/lib/widgets/text.dart @@ -0,0 +1,31 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +class TooltipText extends StatelessWidget { + final Text text; + + const TooltipText({ + super.key, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, container) { + final maxWidth = container.maxWidth; + final size = context.appController.measure.computeTextSize( + text, + ); + if (maxWidth < size.width) { + return Tooltip( + preferBelow: false, + message: text.data, + child: text, + ); + } + return text; + }, + ); + } +} diff --git a/lib/widgets/tile_container.dart b/lib/widgets/tile_container.dart new file mode 100644 index 0000000..0e9a4a0 --- /dev/null +++ b/lib/widgets/tile_container.dart @@ -0,0 +1,48 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/plugins/tile.dart'; +import 'package:flutter/material.dart'; + +class TileContainer extends StatefulWidget { + final Widget child; + + const TileContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _TileContainerState(); +} + +class _TileContainerState extends State with TileListener { + + + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void onStart() { + context.appController.updateSystemProxy(true); + super.onStart(); + } + + @override + void onStop() { + context.appController.updateSystemProxy(false); + super.onStop(); + } + + @override + void initState() { + super.initState(); + tile?.addListener(this); + } + + @override + void dispose() { + tile?.removeListener(this); + super.dispose(); + } +} diff --git a/lib/widgets/tray_container.dart b/lib/widgets/tray_container.dart new file mode 100644 index 0000000..e2ae903 --- /dev/null +++ b/lib/widgets/tray_container.dart @@ -0,0 +1,172 @@ +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:tray_manager/tray_manager.dart'; + +class TrayContainer extends StatefulWidget { + final Widget child; + + const TrayContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _TrayContainerState(); +} + +class _TrayContainerState extends State with TrayListener { + var isTrayInit = false; + + @override + void initState() { + super.initState(); + trayManager.addListener(this); + } + + _updateOtherTray() async { + if (isTrayInit == false) { + await trayManager.setIcon( + Other.getTrayIconPath(), + ); + isTrayInit = true; + } + } + + _updateLinuxTray() async { + await trayManager.destroy(); + await trayManager.setIcon( + Other.getTrayIconPath(), + ); + } + + updateMenu(TrayContainerSelectorState state) async { + if (!Platform.isLinux) { + _updateOtherTray(); + } + List menuItems = []; + final showMenuItem = MenuItem( + label: appLocalizations.show, + onClick: (_) { + window?.show(); + }, + ); + menuItems.add(showMenuItem); + menuItems.add(MenuItem.separator()); + // for (final group in state.groups) { + // List subMenuItems = []; + // final isCurrentGroup = group.name == state.currentGroupName; + // for (final proxy in group.all) { + // final isCurrentProxy = proxy.name == state.currentProxyName; + // subMenuItems.add( + // MenuItem.checkbox( + // label: proxy.name, + // checked: isCurrentGroup && isCurrentProxy, + // onClick: (_) { + // final config = context.appController.config; + // config.currentProfile?.groupName = group.name; + // config.currentProfile?.proxyName = proxy.name; + // config.update(); + // context.appController.changeProxy(); + // }), + // ); + // } + // menuItems.add( + // MenuItem.submenu( + // label: group.name, + // submenu: Menu( + // items: subMenuItems, + // ), + // ), + // ); + // } + // if (state.groups.isNotEmpty) { + // menuItems.add(MenuItem.separator()); + // } + for (final mode in Mode.values) { + menuItems.add( + MenuItem.checkbox( + label: Intl.message(mode.name), + onClick: (_) { + context.appController.clashConfig.mode = mode; + }, + checked: mode == state.mode, + ), + ); + } + menuItems.add(MenuItem.separator()); + final proxyMenuItem = MenuItem.checkbox( + label: appLocalizations.systemProxy, + onClick: (_) async { + context.appController.updateSystemProxy(!state.isRun); + }, + checked: state.isRun, + ); + menuItems.add(proxyMenuItem); + final autoStartMenuItem = MenuItem.checkbox( + label: appLocalizations.autoLaunch, + onClick: (_) async { + context.appController.config.autoLaunch = + !context.appController.config.autoLaunch; + }, + checked: state.autoLaunch, + ); + menuItems.add(autoStartMenuItem); + menuItems.add(MenuItem.separator()); + final exitMenuItem = MenuItem( + label: appLocalizations.exit, + onClick: (_) async { + await context.appController.handleExit(); + }, + ); + menuItems.add(exitMenuItem); + final menu = Menu(); + menu.items = menuItems; + trayManager.setContextMenu(menu); + if (Platform.isLinux) { + _updateLinuxTray(); + } + } + + @override + Widget build(BuildContext context) { + return Selector3( + selector: (_, appState, config, clashConfig) => + TrayContainerSelectorState( + mode: clashConfig.mode, + autoLaunch: config.autoLaunch, + isRun: appState.runTime != null, + locale: config.locale, + ), + builder: (_, state, child) { + debugPrint("[TrayContainer] update===>"); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + updateMenu(state); + }); + return child!; + }, + child: widget.child, + ); + } + + @override + void onTrayIconRightMouseDown() { + trayManager.popUpContextMenu(); + } + + @override + onTrayIconMouseDown() { + window?.show(); + } + + @override + dispose() { + trayManager.removeListener(this); + super.dispose(); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart new file mode 100644 index 0000000..0e6844f --- /dev/null +++ b/lib/widgets/widgets.dart @@ -0,0 +1,25 @@ +export 'scaffold.dart'; +export 'float_layout.dart'; +export 'popup_menu.dart'; +export 'card.dart'; +export 'list.dart'; +export 'line_chart.dart'; +export 'grid.dart'; +export 'open_container.dart'; +export 'color_scheme_box.dart'; +export 'null_status.dart'; +export 'pop_container.dart'; +export 'disabled_mask.dart'; +export 'side_sheet.dart'; +export 'extend_page.dart'; +export 'keep_container.dart'; +export 'animate_grid.dart'; +export 'tray_container.dart'; +export 'window_container.dart'; +export 'android_container.dart'; +export 'clash_message_container.dart'; +export 'tile_container.dart'; +export 'chip.dart'; +export 'fade_box.dart'; +export 'app_state_container.dart'; +export 'text.dart'; \ No newline at end of file diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart new file mode 100644 index 0000000..3d7ee51 --- /dev/null +++ b/lib/widgets/window_container.dart @@ -0,0 +1,47 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowContainer extends StatefulWidget { + final Widget child; + + const WindowContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _WindowContainerState(); +} + +class _WindowContainerState extends State + with WindowListener { + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void initState() { + super.initState(); + windowManager.addListener(this); + } + + @override + void onWindowClose() async { + await context.appController.handleBackOrExit(); + super.onWindowClose(); + } + + @override + void onWindowMinimize() async { + await context.appController.savePreferences(); + super.onWindowMinimize(); + } + + @override + Future dispose() async { + windowManager.removeListener(this); + super.dispose(); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..c237156 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,144 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "FlClash") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.follow.clash") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# libclash.so +set(CLASH_DIR "../libclash/linux/amd64") +install(FILES "${CLASH_DIR}/libclash.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..9474aa0 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..38732ad --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + gtk + screen_retriever + tray_manager + url_launcher_linux + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..4c7b542 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,102 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "FlClash"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "FlClash"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_realize(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + nullptr)); } diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml new file mode 100644 index 0000000..7014159 --- /dev/null +++ b/linux/packaging/appimage/make_config.yaml @@ -0,0 +1,19 @@ +display_name: FlClash + +icon: ./assets/images/launch_icon.png + +keywords: + - FlClash + - Clash + - ClashMeta + - Proxy + +generic_name: FlClash + + +categories: + - Network + +startup_notify: true + +include: [] \ No newline at end of file diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml new file mode 100644 index 0000000..36b6957 --- /dev/null +++ b/linux/packaging/deb/make_config.yaml @@ -0,0 +1,25 @@ +display_name: FlClash +package_name: FlClash +maintainer: + name: chen08209 + email: chen08209@gmail.com + +priority: optional +section: x11 +installed_size: 6604 +essential: false +icon: ./assets/images/launch_icon.png + + +keywords: + - FlClash + - Clash + - ClashMeta + - Proxy + +generic_name: FlClash + +categories: + - Network + +startup_notify: true \ No newline at end of file diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml new file mode 100644 index 0000000..57ccc67 --- /dev/null +++ b/linux/packaging/rpm/make_config.yaml @@ -0,0 +1,23 @@ +display_name: FlClash + +packager: chen08209 +packagerEmail: chen08209@gmail.com +license: Other + +priority: optional +section: x11 +installed_size: 6604 +essential: false +icon: ./assets/images/launch_icon.png + +keywords: + - FlClash + - Clash + - ClashMeta + - Proxy + +generic_name: FlClash + +group: Applications/Internet + +startup_notify: true \ No newline at end of file diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..5048eea --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,30 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import dynamic_color +import mobile_scanner +import package_info_plus +import path_provider_foundation +import screen_retriever +import shared_preferences_foundation +import tray_manager +import url_launcher_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100755 index 0000000..b0a231e --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,804 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 5377B2253E1C5AB4D9D56A31 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */; }; + 7AC277AA2B90DE1400E026B1 /* libclash.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AC277A92B90DE1400E026B1 /* libclash.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; + 7AC277AB2B90DFD900E026B1 /* libclash.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 7AC277A92B90DE1400E026B1 /* libclash.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 7AC6855B2B8AF836004C123B /* (null) in Bundle Framework */ = {isa = PBXBuildFile; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 7AC277AB2B90DFD900E026B1 /* libclash.dylib in Bundle Framework */, + 7AC6855B2B8AF836004C123B /* (null) in Bundle Framework */, + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* FlClash.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlClash.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 391D53BFAED9DD921DE2681C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 779829C96DE7998FCC810C37 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AC277A92B90DE1400E026B1 /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../libclash/macos/amd64/libclash.dylib; sourceTree = ""; }; + 7AF070893C29500AB9129D89 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D929F2AFD80E155D78F3718 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 8F1D6D6423063FA738863205 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + CA9CA9C2D0B5E93A91F45924 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5377B2253E1C5AB4D9D56A31 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7AC277AA2B90DE1400E026B1 /* libclash.dylib in Frameworks */, + CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 89B479066870C6FCC9EB240B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* FlClash.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 89B479066870C6FCC9EB240B /* Pods */ = { + isa = PBXGroup; + children = ( + 7AF070893C29500AB9129D89 /* Pods-Runner.debug.xcconfig */, + 391D53BFAED9DD921DE2681C /* Pods-Runner.release.xcconfig */, + 779829C96DE7998FCC810C37 /* Pods-Runner.profile.xcconfig */, + 7D929F2AFD80E155D78F3718 /* Pods-RunnerTests.debug.xcconfig */, + 8F1D6D6423063FA738863205 /* Pods-RunnerTests.release.xcconfig */, + CA9CA9C2D0B5E93A91F45924 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7AC277A92B90DE1400E026B1 /* libclash.dylib */, + 4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */, + 72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + F99797C7DFD1C67140B8C749 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E94087E39E2BA74B2566D9D6 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 1522C6AC211009D2A7DFAD40 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* FlClash.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1522C6AC211009D2A7DFAD40 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + E94087E39E2BA74B2566D9D6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F99797C7DFD1C67140B8C749 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7D929F2AFD80E155D78F3718 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlClash.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FlClash"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8F1D6D6423063FA738863205 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlClash.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FlClash"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA9CA9C2D0B5E93A91F45924 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlClash.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FlClash"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/"; + PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/"; + PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/"; + PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 0000000..812f5b2 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100755 index 0000000..6e3b8ca --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,23 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return false + } + + override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if !flag { + for window in NSApp.windows { + if !window.isVisible { + window.setIsVisible(true) + } + window.makeKeyAndOrderFront(self) + NSApp.activate(ignoringOtherApps: true) + } + } + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100755 index 0000000..256bf7e Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100755 index 0000000..1d85f13 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100755 index 0000000..c862e32 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100755 index 0000000..04c55dd Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100755 index 0000000..79cf494 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100755 index 0000000..42c6d2c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100755 index 0000000..7a59386 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Assets.xcassets/Contents.json b/macos/Runner/Assets.xcassets/Contents.json new file mode 100755 index 0000000..73c0059 --- /dev/null +++ b/macos/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100755 index 0000000..1319b2e --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100755 index 0000000..776ce24 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = FlClash + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.follow. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100755 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100755 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100755 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100755 index 0000000..d35e43a --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100755 index 0000000..821aec8 --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + CFBundleURLSchemes + + clash + clashmeta + flclash + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100755 index 0000000..c48170a --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,20 @@ +import Cocoa +import FlutterMacOS +import window_manager + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100755 index 0000000..6631ffa --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,6 @@ + + + + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/macos/packaging/dmg/make_config.yaml b/macos/packaging/dmg/make_config.yaml new file mode 100644 index 0000000..1340cad --- /dev/null +++ b/macos/packaging/dmg/make_config.yaml @@ -0,0 +1,10 @@ +title: FlClash +contents: + - x: 448 + y: 344 + type: link + path: "/Applications" + - x: 192 + y: 344 + type: file + path: FlClash.app diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor new file mode 160000 index 0000000..e31c994 --- /dev/null +++ b/plugins/flutter_distributor @@ -0,0 +1 @@ +Subproject commit e31c994e670f35433f3b5840598b4973aad3db3f diff --git a/plugins/proxy/.gitignore b/plugins/proxy/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/plugins/proxy/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/proxy/.metadata b/plugins/proxy/.metadata new file mode 100644 index 0000000..18be979 --- /dev/null +++ b/plugins/proxy/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + - platform: android + create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + - platform: windows + create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/plugins/proxy/CHANGELOG.md b/plugins/proxy/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/plugins/proxy/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/plugins/proxy/LICENSE b/plugins/proxy/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/plugins/proxy/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/plugins/proxy/README.md b/plugins/proxy/README.md new file mode 100644 index 0000000..2475e88 --- /dev/null +++ b/plugins/proxy/README.md @@ -0,0 +1,15 @@ +# proxy + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/plugins/proxy/analysis_options.yaml b/plugins/proxy/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/plugins/proxy/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/plugins/proxy/lib/proxy.dart b/plugins/proxy/lib/proxy.dart new file mode 100644 index 0000000..68af57a --- /dev/null +++ b/plugins/proxy/lib/proxy.dart @@ -0,0 +1,208 @@ +import 'dart:io'; + +import 'proxy_platform_interface.dart'; +import "package:path/path.dart"; + +enum ProxyTypes { http, https, socks } + +class Proxy extends ProxyPlatform { + static String url = "127.0.0.1"; + + @override + Future startProxy(int port, String? args) async { + bool? isStart = false; + switch (Platform.operatingSystem) { + case "macos": + isStart = await _startProxyWithMacos(port); + break; + case "linux": + isStart = await _startProxyWithLinux(port); + break; + case "windows": + isStart = await ProxyPlatform.instance.startProxy(port, args); + break; + } + if (isStart == true) { + startTime = DateTime.now(); + } + return isStart; + } + + @override + Future stopProxy() async { + bool? isStop = false; + switch (Platform.operatingSystem) { + case "macos": + isStop = await _stopProxyWithMacos(); + break; + case "linux": + isStop = await _stopProxyWithLinux(); + break; + case "windows": + isStop = await ProxyPlatform.instance.stopProxy(); + break; + } + if (isStop == true) { + startTime = null; + } + return isStop; + } + + @override + get startTime => ProxyPlatform.instance.startTime; + + @override + set startTime(DateTime? dateTime) { + ProxyPlatform.instance.startTime = dateTime; + } + + Future _startProxyWithLinux(int port) async { + try { + final homeDir = Platform.environment['HOME']!; + final configDir = join(homeDir, ".config"); + final cmdList = List>.empty(growable: true); + final desktop = Platform.environment['XDG_CURRENT_DESKTOP']; + final isKDE = desktop == "KDE"; + for (final type in ProxyTypes.values) { + cmdList.add( + ["gsettings", "set", "org.gnome.system.proxy", "mode", "manual"], + ); + cmdList.add( + [ + "gsettings", + "set", + "org.gnome.system.proxy.${type.name}", + "host", + url + ], + ); + cmdList.add( + [ + "gsettings", + "set", + "org.gnome.system.proxy.${type.name}", + "port", + "$port" + ], + ); + if (isKDE) { + cmdList.add( + [ + "kwriteconfig5", + "--file", + "$configDir/kioslaverc", + "--group", + "Proxy Settings", + "--key", + "ProxyType", + "1" + ], + ); + cmdList.add( + [ + "kwriteconfig5", + "--file", + "$configDir/kioslaverc", + "--group", + "Proxy Settings", + "--key", + "${type.name}Proxy", + "${type.name}://$url:$port" + ], + ); + } + } + for (final cmd in cmdList) { + await Process.run(cmd[0], cmd.sublist(1), runInShell: true); + } + return true; + } catch (_) { + return false; + } + } + + Future _stopProxyWithLinux() async { + try { + final homeDir = Platform.environment['HOME']!; + final configDir = join(homeDir, ".config/"); + final cmdList = List>.empty(growable: true); + final desktop = Platform.environment['XDG_CURRENT_DESKTOP']; + final isKDE = desktop == "KDE"; + cmdList + .add(["gsettings", "set", "org.gnome.system.proxy", "mode", "none"]); + if (isKDE) { + cmdList.add([ + "kwriteconfig5", + "--file", + "$configDir/kioslaverc", + "--group", + "Proxy Settings", + "--key", + "ProxyType", + "0" + ]); + } + for (final cmd in cmdList) { + await Process.run(cmd[0], cmd.sublist(1)); + } + return true; + } catch (_) { + return false; + } + } + + Future _startProxyWithMacos(int port) async { + try { + final devices = await _getNetworkDeviceListWithMacos(); + for (final dev in devices) { + await Future.wait([ + Process.run( + "/usr/sbin/networksetup", ["-setwebproxystate", dev, "on"]), + Process.run( + "/usr/sbin/networksetup", ["-setwebproxy", dev, url, "$port"]), + Process.run( + "/usr/sbin/networksetup", ["-setsecurewebproxystate", dev, "on"]), + Process.run("/usr/sbin/networksetup", + ["-setsecurewebproxy", dev, url, "$port"]), + Process.run("/usr/sbin/networksetup", + ["-setsocksfirewallproxystate", dev, "on"]), + Process.run("/usr/sbin/networksetup", + ["-setsocksfirewallproxy", dev, url, "$port"]), + ]); + } + return true; + } catch (e) { + return false; + } + } + + Future _stopProxyWithMacos() async { + try { + final devices = await _getNetworkDeviceListWithMacos(); + for (final dev in devices) { + await Future.wait([ + Process.run( + "/usr/sbin/networksetup", ["-setautoproxystate", dev, "off"]), + Process.run( + "/usr/sbin/networksetup", ["-setwebproxystate", dev, "off"]), + Process.run("/usr/sbin/networksetup", + ["-setsecurewebproxystate", dev, "off"]), + Process.run("/usr/sbin/networksetup", + ["-setsocksfirewallproxystate", dev, "off"]), + ]); + } + return true; + } catch (e) { + return false; + } + } + + Future> _getNetworkDeviceListWithMacos() async { + final res = await Process.run( + "/usr/sbin/networksetup", ["-listallnetworkservices"]); + final lines = res.stdout.toString().split("\n"); + lines.removeWhere((element) => element.contains("*")); + return lines; + } +} + diff --git a/plugins/proxy/lib/proxy_method_channel.dart b/plugins/proxy/lib/proxy_method_channel.dart new file mode 100644 index 0000000..f7b98e7 --- /dev/null +++ b/plugins/proxy/lib/proxy_method_channel.dart @@ -0,0 +1,27 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'proxy_platform_interface.dart'; + +/// An implementation of [ProxyPlatform] that uses method channels. +class MethodChannelProxy extends ProxyPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('proxy'); + + MethodChannelProxy(); + + @override + Future startProxy(int port, String? args) async { + return await methodChannel.invokeMethod("StartProxy", {'port': port}); + } + + @override + Future stopProxy() async { + final isStop = await methodChannel.invokeMethod("StopProxy"); + if (isStop == true) { + startTime = null; + } + return isStop; + } +} diff --git a/plugins/proxy/lib/proxy_platform_interface.dart b/plugins/proxy/lib/proxy_platform_interface.dart new file mode 100644 index 0000000..cab76d3 --- /dev/null +++ b/plugins/proxy/lib/proxy_platform_interface.dart @@ -0,0 +1,32 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'proxy_method_channel.dart'; + +abstract class ProxyPlatform extends PlatformInterface { + /// Constructs a ProxyPlatform. + ProxyPlatform() : super(token: _token); + + static final Object _token = Object(); + + static ProxyPlatform _instance = MethodChannelProxy(); + + /// The default instance of [ProxyPlatform] to use. + /// + /// Defaults to [MethodChannelProxy]. + static ProxyPlatform get instance => _instance; + + static set instance(ProxyPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + DateTime? startTime; + + Future startProxy(int port, String? args) { + throw UnimplementedError('startProxy() has not been implemented.'); + } + + Future stopProxy() { + throw UnimplementedError('stopProxy() has not been implemented.'); + } +} diff --git a/plugins/proxy/pubspec.yaml b/plugins/proxy/pubspec.yaml new file mode 100644 index 0000000..9236ca4 --- /dev/null +++ b/plugins/proxy/pubspec.yaml @@ -0,0 +1,70 @@ +name: proxy +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.1.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + path: ^1.8.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + windows: + pluginClass: ProxyPluginCApi + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/plugins/proxy/windows/.gitignore b/plugins/proxy/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/plugins/proxy/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/plugins/proxy/windows/.vscode/settings.json b/plugins/proxy/windows/.vscode/settings.json new file mode 100644 index 0000000..9498efe --- /dev/null +++ b/plugins/proxy/windows/.vscode/settings.json @@ -0,0 +1,53 @@ +{ + "files.associations": { + "*.ftl": "vue-html", + "*.vue": "vue", + "*.scss": "sass", + "*.md": "markdown", + ".gitignore": "ignore", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "exception": "cpp", + "initializer_list": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "iterator": "cpp", + "limits": "cpp", + "memory": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "utility": "cpp", + "variant": "cpp", + "xfacet": "cpp", + "xiosbase": "cpp", + "xlocale": "cpp", + "xlocinfo": "cpp", + "xlocnum": "cpp", + "xmemory": "cpp", + "xstddef": "cpp", + "xstring": "cpp", + "xtr1common": "cpp", + "xutility": "cpp" + } +} diff --git a/plugins/proxy/windows/CMakeLists.txt b/plugins/proxy/windows/CMakeLists.txt new file mode 100644 index 0000000..82f7b79 --- /dev/null +++ b/plugins/proxy/windows/CMakeLists.txt @@ -0,0 +1,100 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "proxy") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "proxy_plugin") + +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "proxy_plugin.cpp" + "proxy_plugin.h" +) + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +add_library(${PLUGIN_NAME} SHARED + "include/proxy/proxy_plugin_c_api.h" + "proxy_plugin_c_api.cpp" + ${PLUGIN_SOURCES} +) + +# Apply a standard set of build settings that are configured in the +# application-level CMakeLists.txt. This can be removed for plugins that want +# full control over build settings. +apply_standard_settings(${PLUGIN_NAME}) + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(proxy_bundled_libraries + "" + PARENT_SCOPE +) + +# === Tests === +# These unit tests can be run from a terminal after building the example, or +# from Visual Studio after opening the generated solution file. + +# Only enable test builds when building the example (which sets this variable) +# so that plugin clients aren't building the tests. +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() + +# Add the Google Test dependency. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/proxy_plugin_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +# Enable automatic test discovery. +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h b/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h new file mode 100644 index 0000000..3eca010 --- /dev/null +++ b/plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ diff --git a/plugins/proxy/windows/proxy_plugin.cpp b/plugins/proxy/windows/proxy_plugin.cpp new file mode 100644 index 0000000..9f4aa12 --- /dev/null +++ b/plugins/proxy/windows/proxy_plugin.cpp @@ -0,0 +1,171 @@ +#include "proxy_plugin.h" + +// This must be included before many other Windows headers. +#include + +#include +#include +#include +#include +#include + +#pragma comment(lib, "wininet") +#pragma comment(lib, "Rasapi32") + +// For getPlatformVersion; remove unless needed for your plugin implementation. +#include + +#include +#include +#include + +#include +#include + +void startProxy(const int port) +{ + INTERNET_PER_CONN_OPTION_LIST list; + DWORD dwBufSize = sizeof(list); + list.dwSize = sizeof(list); + list.pszConnection = nullptr; + auto url = "127.0.0.1:" + std::to_string(port); + auto wUrl = std::wstring(url.begin(), url.end()); + auto fullAddr = new WCHAR[url.length() + 1]; + wcscpy_s(fullAddr, url.length() + 1, wUrl.c_str()); + list.dwOptionCount = 2; + list.pOptions = new INTERNET_PER_CONN_OPTION[2]; + + if (!list.pOptions) + { + return; + } + + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_PROXY; + + list.pOptions[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + list.pOptions[1].Value.pszValue = fullAddr; + + InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); + + RASENTRYNAME entry; + entry.dwSize = sizeof(entry); + std::vector entries; + DWORD size = sizeof(entry), count; + LPRASENTRYNAME entryAddr = &entry; + auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + if (ret == ERROR_BUFFER_TOO_SMALL) + { + entries.resize(count); + entries[0].dwSize = sizeof(RASENTRYNAME); + entryAddr = entries.data(); + ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + } + if (ret != ERROR_SUCCESS) + { + return; + } + for (DWORD i = 0; i < count; i++) + { + list.pszConnection = entryAddr[i].szEntryName; + InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); + } + delete[] list.pOptions; + InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); + InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); +} + +void stopProxy() +{ + INTERNET_PER_CONN_OPTION_LIST list; + DWORD dwBufSize = sizeof(list); + + list.dwSize = sizeof(list); + list.pszConnection = nullptr; + list.dwOptionCount = 1; + list.pOptions = new INTERNET_PER_CONN_OPTION[1]; + if (nullptr == list.pOptions) + { + return; + } + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT; + + InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); + + RASENTRYNAME entry; + entry.dwSize = sizeof(entry); + std::vector entries; + DWORD size = sizeof(entry), count; + LPRASENTRYNAME entryAddr = &entry; + auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + if (ret == ERROR_BUFFER_TOO_SMALL) + { + entries.resize(count); + entries[0].dwSize = sizeof(RASENTRYNAME); + entryAddr = entries.data(); + ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + } + if (ret != ERROR_SUCCESS) + { + return; + } + for (DWORD i = 0; i < count; i++) + { + list.pszConnection = entryAddr[i].szEntryName; + InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize); + } + delete[] list.pOptions; + InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); + InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); +} + +namespace proxy +{ + + // static + void ProxyPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) + { + auto channel = + std::make_unique>( + registrar->messenger(), "proxy", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) + { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } + + ProxyPlugin::ProxyPlugin() {} + + ProxyPlugin::~ProxyPlugin() {} + + void ProxyPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) + { + if (method_call.method_name().compare("StopProxy") == 0) + { + stopProxy(); + result->Success(true); + } + else if (method_call.method_name().compare("StartProxy") == 0) + { + auto *arguments = std::get_if(method_call.arguments()); + auto port = std::get(arguments->at(flutter::EncodableValue("port"))); + startProxy(port); + result->Success(true); + } + else + { + result->NotImplemented(); + } + } +} // namespace proxy diff --git a/plugins/proxy/windows/proxy_plugin.h b/plugins/proxy/windows/proxy_plugin.h new file mode 100644 index 0000000..ae3ff1a --- /dev/null +++ b/plugins/proxy/windows/proxy_plugin.h @@ -0,0 +1,31 @@ +#ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_H_ +#define FLUTTER_PLUGIN_PROXY_PLUGIN_H_ + +#include +#include + +#include + +namespace proxy { + +class ProxyPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + ProxyPlugin(); + + virtual ~ProxyPlugin(); + + // Disallow copy and assign. + ProxyPlugin(const ProxyPlugin&) = delete; + ProxyPlugin& operator=(const ProxyPlugin&) = delete; + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); +}; + +} // namespace proxy + +#endif // FLUTTER_PLUGIN_PROXY_PLUGIN_H_ diff --git a/plugins/proxy/windows/proxy_plugin_c_api.cpp b/plugins/proxy/windows/proxy_plugin_c_api.cpp new file mode 100644 index 0000000..4f39877 --- /dev/null +++ b/plugins/proxy/windows/proxy_plugin_c_api.cpp @@ -0,0 +1,12 @@ +#include "include/proxy/proxy_plugin_c_api.h" + +#include + +#include "proxy_plugin.h" + +void ProxyPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + proxy::ProxyPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/plugins/proxy/windows/test/proxy_plugin_test.cpp b/plugins/proxy/windows/test/proxy_plugin_test.cpp new file mode 100644 index 0000000..7f935fe --- /dev/null +++ b/plugins/proxy/windows/test/proxy_plugin_test.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "proxy_plugin.h" + +namespace proxy { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using flutter::MethodCall; +using flutter::MethodResultFunctions; + +} // namespace + +TEST(ProxyPlugin, GetPlatformVersion) { + ProxyPlugin plugin; + // Save the reply value from the success callback. + std::string result_string; + plugin.HandleMethodCall( + MethodCall("getPlatformVersion", std::make_unique()), + std::make_unique>( + [&result_string](const EncodableValue* result) { + result_string = std::get(*result); + }, + nullptr, nullptr)); + + // Since the exact string varies by host, just ensure that it's a string + // with the expected format. + EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); +} + +} // namespace test +} // namespace proxy diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..d654ac7 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1006 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.4.1" + animations: + dependency: "direct main" + description: + name: animations + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.1" + args: + dependency: "direct dev" + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.6" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: "direct main" + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: dead012f29db2be71ea152458f5eab600de98fbc244e01088ae6bf2616bceca7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_adaptive_scaffold: + dependency: "direct main" + description: + name: flutter_adaptive_scaffold + sha256: "600bbe237530a249f957f7d0f36273c20bd38d137e28e098c5231c30cadbe927" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.10+1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "592dc01a18961a51c24ae5d963b724b2b7fa4a95c100fe8eb6ca8a5a4732cadf" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.18" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "91bce569d4805ea5bad6619a3e8690df8ad062a235165af4c0c5d928dda15eaf" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + http: + dependency: "direct main" + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.8.0" + launch_at_startup: + dependency: "direct main" + description: + name: launch_at_startup + sha256: "93fc5638e088290004fae358bae691486673d469957d461d9dae5b12248593eb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.0" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.1" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: f34c83198d9381f6c100dfaec647c275630840cbcda5d6c5eb6ba264beb96be4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "2c582551839386fa7ddbc7770658be7c0f87f388a4bff72066478f597c34d17f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.2" + proxy: + dependency: "direct main" + description: + path: "plugins/proxy" + relative: true + source: path + version: "0.0.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.3" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.9" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: e0ac9a88b2700f366b8629b97e8663b6ef450a2f169560a685dc167bfe9c9c29 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.5" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.5.0" + win32_registry: + dependency: "direct main" + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.8" + windows_single_instance: + dependency: "direct main" + description: + name: windows_single_instance + sha256: "50d5dcd6bec90b4a5ed588b1822b1aad21b39fc96da843e61c734b3caccfd2fc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: c566f4f804215d84a7a2c377667f546c6033d5b34b4f9e60dfb09d17c4e97826 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..69d4ed7 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,63 @@ +name: fl_clash +description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. +publish_to: 'none' +version: 0.7.0 +environment: + sdk: '>=3.1.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.18.1 + path_provider: ^2.1.0 + path: ^1.8.3 + shared_preferences: ^2.2.0 + provider: ^6.0.5 + window_manager: ^0.3.8 + ffi: ^2.1.0 + dynamic_color: ^1.7.0 + proxy: + path: plugins/proxy + launch_at_startup: ^0.2.2 + windows_single_instance: ^1.0.1 + json_annotation: ^4.9.0 + http: ^1.2.0 + file_picker: ^6.1.1 + mobile_scanner: ^5.0.1 + app_links: ^3.5.0 + win32_registry: ^1.1.2 + tray_manager: ^0.2.1 + collection: ^1.18.0 + animations: ^2.0.11 + package_info_plus: ^7.0.0 + url_launcher: ^6.2.6 + flutter_adaptive_scaffold: ^0.1.10+1 + freezed_annotation: ^2.4.1 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + ffigen: ^11.0.0 + json_serializable: ^6.7.1 + build_runner: ^2.4.9 + args: ^2.4.2 + freezed: ^2.5.1 + +flutter: + uses-material-design: true + assets: + - assets/data/geoip.metadb + - assets/images/ +ffigen: + name: "ClashFFI" + output: 'lib/clash/generated/clash_ffi.dart' + headers: + entry-points: + - 'libclash/android/arm64-v8a/libclash.h' +flutter_intl: + enabled: true + class_name: AppLocalizations + arb_dir: lib/l10n/arb + output_dir: lib/l10n \ No newline at end of file diff --git a/setup.dart b/setup.dart new file mode 100644 index 0000000..cf47e6a --- /dev/null +++ b/setup.dart @@ -0,0 +1,403 @@ +// ignore_for_file: avoid_print + +import 'dart:convert'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; + +enum PlatformType { + windows, + linux, + android, + macos, +} + +enum Arch { + amd64, + arm64, +} + +class BuildLibItem { + PlatformType platform; + Arch arch; + String archName; + + BuildLibItem({ + required this.platform, + required this.arch, + required this.archName, + }); + + String get dynamicLibExtensionName { + final String extensionName; + switch (platform) { + case PlatformType.android || PlatformType.linux: + extensionName = "so"; + break; + case PlatformType.windows: + extensionName = "dll"; + break; + case PlatformType.macos: + extensionName = "dylib"; + break; + } + return extensionName; + } + + String get os { + if (platform == PlatformType.macos) { + return "darwin"; + } + return platform.name; + } +} + +class Build { + static List get buildItems => [ + BuildLibItem( + platform: PlatformType.macos, + arch: Arch.amd64, + archName: 'amd64', + ), + BuildLibItem( + platform: PlatformType.windows, + arch: Arch.amd64, + archName: 'amd64', + ), + BuildLibItem( + platform: PlatformType.android, + arch: Arch.arm64, + archName: 'arm64-v8a', + ), + BuildLibItem( + platform: PlatformType.android, + arch: Arch.amd64, + archName: 'x86_64', + ), + BuildLibItem( + platform: PlatformType.linux, + arch: Arch.amd64, + archName: 'amd64', + ), + ]; + + static String get appName => "FlClash"; + + static String get libName => "libclash"; + + static String get outDir => join(current, libName); + + static String get _coreDir => join(current, "core"); + + static String get distPath => join(current, "dist"); + + static String _getCc(BuildLibItem buildItem) { + final environment = Platform.environment; + if (buildItem.platform == PlatformType.android) { + final ndk = environment["ANDROID_NDK"]; + assert(ndk != null); + final prebuiltDir = + Directory(join(ndk!, "toolchains", "llvm", "prebuilt")); + final prebuiltDirList = prebuiltDir.listSync(); + final map = { + "armeabi-v7a": "armv7a-linux-androideabi21-clang", + "arm64-v8a": "aarch64-linux-android21-clang", + "x86": "i686-linux-android21-clang", + "x86_64": "x86_64-linux-android21-clang" + }; + return join( + prebuiltDirList.first.path, + "bin", + map[buildItem.archName], + ); + } + return "gcc"; + } + + static get tags => "with_gvisor"; + + static Future exec( + List executable, { + String? name, + Map? environment, + String? workingDirectory, + bool runInShell = true, + }) async { + if (name != null) print("run $name"); + final process = await Process.start( + executable[0], + executable.sublist(1), + environment: environment, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); + process.stdout.listen((data) { + print(utf8.decode(data)); + }); + process.stderr.listen((data) { + print(utf8.decode(data)); + }); + final exitCode = await process.exitCode; + if (exitCode != 0 && name != null) throw "$name error"; + } + + static buildLib({ + required PlatformType platform, + Arch? arch, + }) async { + final items = buildItems.where( + (element) { + return element.platform == platform && arch == null + ? true + : element.arch == arch; + }, + ).toList(); + for (final item in items) { + final outFileDir = join( + outDir, + item.platform.name, + item.archName, + ); + final file = File(outFileDir); + if (file.existsSync()) { + file.deleteSync(recursive: true); + } + final outPath = join( + outFileDir, + "$libName.${item.dynamicLibExtensionName}", + ); + final Map env = {}; + env["GOOS"] = item.os; + env["GOARCH"] = item.arch.name; + env["CGO_ENABLED"] = "1"; + env["CC"] = _getCc(item); + if (item.platform == PlatformType.macos) { + env["CGO_CFLAGS"] = "-mmacosx-version-min=10.11"; + env["CGO_LDFLAGS"] = "-mmacosx-version-min=10.11"; + } + + await exec( + [ + "go", + "build", + "-ldflags=-w -s", + "-tags=$tags", + "-buildmode=c-shared", + "-o", + outPath, + ], + name: "build libclash", + environment: env, + workingDirectory: _coreDir, + ); + } + } + + static List getExecutable(String command) { + return command.split(" "); + } + + static getDistributor() async { + final distributorDir = join( + current, + "plugins", + "flutter_distributor", + "packages", + "flutter_distributor", + ); + + await exec( + name: "clean distributor", + Build.getExecutable("flutter clean"), + workingDirectory: distributorDir, + ); + await exec( + name: "get distributor", + Build.getExecutable("dart pub global activate -s path $distributorDir"), + ); + } + + static copyFile(String sourceFilePath, String destinationFilePath) { + final sourceFile = File(sourceFilePath); + if (!sourceFile.existsSync()) { + throw "SourceFilePath not exists"; + } + final destinationFile = File(destinationFilePath); + final destinationDirectory = destinationFile.parent; + if (!destinationDirectory.existsSync()) { + destinationDirectory.createSync(recursive: true); + } + try { + sourceFile.copySync(destinationFilePath); + print("File copied successfully!"); + } catch (e) { + print("Failed to copy file: $e"); + } + } +} + +class BuildCommand extends Command { + PlatformType platform; + + BuildCommand({ + required this.platform, + }) { + argParser.addOption( + "build", + valueHelp: [ + 'all', + 'lib', + ].join(','), + help: 'The $name build type', + ); + argParser.addOption( + "arch", + valueHelp: arches.map((e) => e.name).join(','), + help: 'The $name build arch', + ); + } + + @override + String get description => "build $name application"; + + @override + String get name => platform.name; + + List get arches => Build.buildItems + .where((element) => element.platform == platform) + .map((e) => e.arch) + .toList(); + + Future _buildLib(Arch? arch) async { + await Build.buildLib(platform: platform, arch: arch); + } + + _getLinuxDependencies() async { + await Build.exec( + Build.getExecutable("sudo apt update -y"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y ninja-build libgtk-3-dev"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y libayatana-appindicator3-dev"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y rpm patchelf"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y locate"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y libfuse2"), + ); + await Build.exec( + Build.getExecutable( + "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage", + ), + ); + await Build.exec( + Build.getExecutable( + "chmod +x appimagetool", + ), + ); + await Build.exec( + Build.getExecutable( + "sudo mv appimagetool /usr/local/bin/", + ), + ); + } + + _getMacosDependencies() async { + await Build.exec( + Build.getExecutable("npm install -g appdmg"), + ); + } + + _buildDistributor({ + required PlatformType platform, + required String targets, + String args = '', + }) async { + await Build.getDistributor(); + await Build.exec( + name: name, + Build.getExecutable( + "flutter_distributor package --skip-clean --platform ${platform.name} --targets $targets --flutter-build-args=verbose $args", + ), + ); + } + + @override + Future run() async { + final String build = argResults?['build'] ?? 'all'; + final archName = argResults?['arch']; + final currentArches = + arches.where((element) => element.name == archName).toList(); + final arch = currentArches.isEmpty ? null : arches.first; + await _buildLib(arch); + if (build != "all") { + return; + } + switch (platform) { + case PlatformType.windows: + _buildDistributor( + platform: platform, + targets: "exe,zip", + args: "--description amd64", + ); + break; + case PlatformType.linux: + await _getLinuxDependencies(); + _buildDistributor( + platform: platform, + targets: "appimage,deb,rpm", + args: "--description amd64", + ); + break; + case PlatformType.android: + final targetMap = { + Arch.amd64: "android-x64", + Arch.arm64: "android-arm64" + }; + final defaultArches = [Arch.amd64, Arch.arm64]; + final defaultTargets = defaultArches + .where((element) => arch == null ? true : element == arch) + .map((e) => targetMap[e]) + .toList(); + _buildDistributor( + platform: platform, + targets: "apk", + args: + "--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}", + ); + break; + case PlatformType.macos: + await _getMacosDependencies(); + _buildDistributor( + platform: platform, + targets: "dmg", + args: "--description amd64", + ); + break; + } + } +} + +main(args) async { + final runner = CommandRunner("setup", "build Application"); + runner.addCommand(BuildCommand(platform: PlatformType.android)); + if (Platform.isWindows) { + runner.addCommand(BuildCommand(platform: PlatformType.windows)); + } + if (Platform.isLinux) { + runner.addCommand(BuildCommand(platform: PlatformType.linux)); + } + if (Platform.isMacOS) { + runner.addCommand(BuildCommand(platform: PlatformType.macos)); + } + if (args.isEmpty) { + args = [Platform.operatingSystem]; + } + runner.run(args); +} diff --git a/snapshots/desktop.gif b/snapshots/desktop.gif new file mode 100644 index 0000000..eaf55c0 Binary files /dev/null and b/snapshots/desktop.gif differ diff --git a/snapshots/mobile.gif b/snapshots/mobile.gif new file mode 100644 index 0000000..3d01cb9 Binary files /dev/null and b/snapshots/mobile.gif differ diff --git a/test/command_test.dart b/test/command_test.dart new file mode 100644 index 0000000..f958fa6 --- /dev/null +++ b/test/command_test.dart @@ -0,0 +1,18 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +main() async { + final result = await Process.run( + 'netstat', + ["-ano","|","findstr",":7890","|","findstr","LISTENING"], + runInShell: true, + ); + final output = result.stdout as String; + final line = output.split('\n').first; + final pid = line.split(' ').firstWhere( + (value) => value.trim().contains(RegExp(r'^\d+$')), + orElse: () => '', + ); + print(pid); +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..137ebe0 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,24 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +build/ + +out/ +.idea/ +.vs/ +.vscode/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..0394ac1 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,114 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(FlClash LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "FlClash") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# libclash.so +set(CLASH_DIR "../libclash/windows/amd64") + +# if(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") +# elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM" OR CMAKE_SYSTEM_PROCESSOR MATCHES "armv[0-9]+") +# elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86") +# set(CLASH_DIR "../libclash/windows/x86") +# endif() + +install(FILES "${CLASH_DIR}/libclash.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..aa445ae --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + ProxyPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ProxyPluginCApi")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); + WindowsSingleInstancePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsSingleInstancePlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..daf5348 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,31 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + dynamic_color + proxy + screen_retriever + tray_manager + url_launcher_windows + window_manager + windows_single_instance +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/packaging/exe/ChineseSimplified.isl b/windows/packaging/exe/ChineseSimplified.isl new file mode 100644 index 0000000..31dd8b1 --- /dev/null +++ b/windows/packaging/exe/ChineseSimplified.isl @@ -0,0 +1,394 @@ +; *** Inno Setup version 6.1.0+ Chinese Simplified messages *** +; +; To download user-contributed translations of this file, go to: +; https://jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). +; +; Maintained by Zhenghan Yang +; Email: 847320916@QQ.com +; Translation based on network resource +; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation +; + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=简体中文 +; If Language Name display incorrect, uncomment next line +; LanguageName=<7B80><4F53><4E2D><6587> +; About LanguageID, to reference link: +; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c +LanguageID=$0804 +LanguageCodePage=936 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +DialogFontName=Microsoft YaHei UI +;DialogFontSize=8 +WelcomeFontName=Microsoft YaHei UI +;WelcomeFontSize=12 +TitleFontName=Microsoft YaHei UI +;TitleFontSize=29 +;CopyrightFontName=Arial +;CopyrightFontSize=8 + +[Messages] + +; *** 应用程序标题 +SetupAppTitle=安装 +SetupWindowTitle=安装 - %1 +UninstallAppTitle=卸载 +UninstallAppFullTitle=%1 卸载 + +; *** Misc. common +InformationTitle=信息 +ConfirmTitle=确认 +ErrorTitle=错误 + +; *** SetupLdr messages +SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? +LdrCannotCreateTemp=不能创建临时文件。安装中断。 +LdrCannotExecTemp=不能执行临时目录中的文件。安装中断。 +HelpTextNote= + +; *** 启动错误消息 +LastErrorMessage=%1.%n%n错误 %2: %3 +SetupFileMissing=安装目录中的文件 %1 丢失。请修正这个问题或者获取程序的新副本。 +SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 +SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 +InvalidParameter=无效的命令行参数:%n%n%1 +SetupAlreadyRunning=安装程序正在运行。 +WindowsVersionNotSupported=这个程序不支持当前计算机运行的 Windows 版本。 +WindowsServicePackRequired=这个程序需要 %1 服务包 %2 或更高。 +NotOnThisPlatform=这个程序将不能运行于 %1。 +OnlyOnThisPlatform=这个程序必须运行于 %1。 +OnlyOnTheseArchitectures=这个程序只能在为下列处理器架构的 Windows 版本中进行安装:%n%n%1 +WinVersionTooLowError=这个程序需要 %1 版本 %2 或更高。 +WinVersionTooHighError=这个程序不能安装于 %1 版本 %2 或更高。 +AdminPrivilegesRequired=在安装这个程序时您必须以管理员身份登录。 +PowerUserPrivilegesRequired=在安装这个程序时您必须以管理员身份或有权限的用户组身份登录。 +SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭所有运行的窗口,然后点击“确定”继续,或按“取消”退出。 +UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭所有运行的窗口,然后点击“确定”继续,或按“取消”退出。 + +; *** 启动问题 +PrivilegesRequiredOverrideTitle=选择安装程序模式 +PrivilegesRequiredOverrideInstruction=选择安装模式 +PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 +PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 +PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) +PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) +PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M) +PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项) + +; *** 其它错误 +ErrorCreatingDir=安装程序不能创建目录“%1”。 +ErrorTooManyFilesInDir=不能在目录“%1”中创建文件,因为里面的文件太多 + +; *** 安装程序公共消息 +ExitSetupTitle=退出安装程序 +ExitSetupMessage=安装程序尚未完成安装。如果您现在退出,程序将不能安装。%n%n您可以以后再运行安装程序完成安装。%n%n现在退出安装程序吗? +AboutSetupMenuItem=关于安装程序(&A)... +AboutSetupTitle=关于安装程序 +AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 +AboutSetupNote= +TranslatorNote=Translated by Zhenghan Yang. + +; *** 按钮 +ButtonBack=< 上一步(&B) +ButtonNext=下一步(&N) > +ButtonInstall=安装(&I) +ButtonOK=确定 +ButtonCancel=取消 +ButtonYes=是(&Y) +ButtonYesToAll=全是(&A) +ButtonNo=否(&N) +ButtonNoToAll=全否(&O) +ButtonFinish=完成(&F) +ButtonBrowse=浏览(&B)... +ButtonWizardBrowse=浏览(&R)... +ButtonNewFolder=新建文件夹(&M) + +; *** “选择语言”对话框消息 +SelectLanguageTitle=选择安装语言 +SelectLanguageLabel=选择安装时要使用的语言。 + +; *** 公共向导文字 +ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 +BeveledLabel= +BrowseDialogTitle=浏览文件夹 +BrowseDialogLabel=在下列列表中选择一个文件夹,然后点击“确定”。 +NewFolderName=新建文件夹 + +; *** “欢迎”向导页 +WelcomeLabel1=欢迎使用 [name] 安装向导 +WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n推荐您在继续安装前关闭所有其它应用程序。 + +; *** “密码”向导页 +WizardPassword=密码 +PasswordLabel1=这个安装程序有密码保护。 +PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 +PasswordEditLabel=密码(&P): +IncorrectPassword=您所输入的密码不正确,请重试。 + +; *** “许可协议”向导页 +WizardLicense=许可协议 +LicenseLabel=继续安装前请阅读下列重要信息。 +LicenseLabel3=请仔细阅读下列许可协议。您在继续安装前必须同意这些协议条款。 +LicenseAccepted=我同意此协议(&A) +LicenseNotAccepted=我拒绝此协议(&D) + +; *** “信息”向导页 +WizardInfoBefore=信息 +InfoBeforeLabel=请在继续安装前阅读下列重要信息。 +InfoBeforeClickLabel=如果您想继续安装,点击“下一步”。 +WizardInfoAfter=信息 +InfoAfterLabel=请在继续安装前阅读下列重要信息。 +InfoAfterClickLabel=如果您想继续安装,点击“下一步”。 + +; *** “用户信息”向导页 +WizardUserInfo=用户信息 +UserInfoDesc=请输入您的信息。 +UserInfoName=用户名(&U): +UserInfoOrg=组织(&O): +UserInfoSerial=序列号(&S): +UserInfoNameRequired=您必须输入用户名。 + +; *** “选择目标目录”向导页 +WizardSelectDir=选择目标位置 +SelectDirDesc=您想将 [name] 安装在哪里? +SelectDirLabel3=安装程序将安装 [name] 到下列文件夹中。 +SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其它文件夹,点击“浏览”。 +DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 +DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 +CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 +CannotInstallToUNCPath=安装程序无法安装到一个UNC路径。 +InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或下列形式的UNC路径:%n%n\\server\share +InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选选择其它位置。 +DiskSpaceWarningTitle=没有足够的磁盘空间 +DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? +DirNameTooLong=文件夹名称或路径太长。 +InvalidDirName=文件夹名称无效。 +BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 +DirExistsTitle=文件夹已存在 +DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? +DirDoesntExistTitle=文件夹不存在 +DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? + +; *** “选择组件”向导页 +WizardSelectComponents=选择组件 +SelectComponentsDesc=您想安装哪些程序的组件? +SelectComponentsLabel2=选择您想要安装的组件;清除您不想安装的组件。然后点击“下一步”继续。 +FullInstallation=完全安装 +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=简洁安装 +CustomInstallation=自定义安装 +NoUninstallWarningTitle=组件已存在 +NoUninstallWarning=安装程序检测到下列组件已在您的电脑中安装:%n%n%1%n%n取消选定这些组件将不能卸载它们。%n%n您一定要继续吗? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceGBLabel=当前选择的组件至少需要 [gb] GB 的磁盘空间。 +ComponentsDiskSpaceMBLabel=当前选择的组件至少需要 [mb] MB 的磁盘空间。 + +; *** “选择附加任务”向导页 +WizardSelectTasks=选择附加任务 +SelectTasksDesc=您想要安装程序执行哪些附加任务? +SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 + +; *** “选择开始菜单文件夹”向导页 +WizardSelectProgramGroup=选择开始菜单文件夹 +SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? +SelectStartMenuFolderLabel3=安装程序现在将在下列开始菜单文件夹中创建程序的快捷方式。 +SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其它文件夹,点击“浏览”。 +MustEnterGroupName=您必须输入一个文件夹名。 +GroupNameTooLong=文件夹名或路径太长。 +InvalidGroupName=文件夹名无效。 +BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 +NoProgramGroupCheck2=不创建开始菜单文件夹(&D) + +; *** “准备安装”向导页 +WizardReady=准备安装 +ReadyLabel1=安装程序现在准备开始安装 [name] 到您的电脑中。 +ReadyLabel2a=点击“安装”继续此安装程序。如果您想要回顾或修改设置,请点击“上一步”。 +ReadyLabel2b=点击“安装”继续此安装程序? +ReadyMemoUserInfo=用户信息: +ReadyMemoDir=目标位置: +ReadyMemoType=安装类型: +ReadyMemoComponents=选定组件: +ReadyMemoGroup=开始菜单文件夹: +ReadyMemoTasks=附加任务: + +; *** TDownloadWizardPage wizard page and DownloadTemporaryFile +DownloadingLabel=正在下载附加文件... +ButtonStopDownload=停止下载(&S) +StopDownload=您确定要停止下载吗? +ErrorDownloadAborted=下载已中止 +ErrorDownloadFailed=下载失败:%1 %2 +ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 +ErrorFileHash1=校验文件哈希失败:%1 +ErrorFileHash2=无效的文件哈希:预期为 %1,实际为 %2 +ErrorProgress=无效的进度:%1,总共%2 +ErrorFileSize=文件大小错误:预期为 %1,实际为 %2 + +; *** “正在准备安装”向导页 +WizardPreparing=正在准备安装 +PreparingDesc=安装程序正在准备安装 [name] 到您的电脑中。 +PreviousInstallNotCompleted=先前程序的安装/卸载未完成。您需要重新启动您的电脑才能完成安装。%n%n在重新启动电脑后,再运行安装完成 [name] 的安装。 +CannotContinue=安装程序不能继续。请点击“取消”退出。 +ApplicationsFound=下列应用程序正在使用的文件需要更新设置。它是建议您允许安装程序自动关闭这些应用程序。 +ApplicationsFound2=下列应用程序正在使用的文件需要更新设置。它是建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动应用程序。 +CloseApplications=自动关闭该应用程序(&A) +DontCloseApplications=不要关闭该应用程序(&D) +ErrorCloseApplications=安装程序无法自动关闭所有应用程序。在继续之前,我们建议您关闭所有使用需要更新的安装程序文件。 +PrepareToInstallNeedsRestart=安装程序必须重新启动计算机。重新启动计算机后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? + +; *** “正在安装”向导页 +WizardInstalling=正在安装 +InstallingLabel=安装程序正在安装 [name] 到您的电脑中,请稍等。 + +; *** “安装完成”向导页 +FinishedHeadingLabel=[name] 安装完成 +FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 +FinishedLabel=安装程序已在您的电脑中安装了 [name]。此应用程序可以通过选择安装的快捷方式运行。 +ClickFinish=点击“完成”退出安装程序。 +FinishedRestartLabel=要完成 [name] 的安装,安装程序必须重新启动您的电脑。您想要立即重新启动吗? +FinishedRestartMessage=要完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n您想要立即重新启动吗? +ShowReadmeCheck=是,我想查阅自述文件 +YesRadio=是,立即重新启动电脑(&Y) +NoRadio=否,稍后重新启动电脑(&N) +; used for example as 'Run MyProg.exe' +RunEntryExec=运行 %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=查阅 %1 + +; *** “安装程序需要下一张磁盘”提示 +ChangeDiskTitle=安装程序需要下一张磁盘 +SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 +PathLabel=路径(&P): +FileNotInDir2=文件“%1”不能在“%2”定位。请插入正确的磁盘或选择其它文件夹。 +SelectDirectoryLabel=请指定下一张磁盘的位置。 + +; *** 安装状态消息 +SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 +AbortRetryIgnoreSelectAction=选择操作 +AbortRetryIgnoreRetry=重试(&T) +AbortRetryIgnoreIgnore=忽略错误并继续(&I) +AbortRetryIgnoreCancel=关闭安装程序 + +; *** 安装状态消息 +StatusClosingApplications=正在关闭应用程序... +StatusCreateDirs=正在创建目录... +StatusExtractFiles=正在解压缩文件... +StatusCreateIcons=正在创建快捷方式... +StatusCreateIniEntries=正在创建 INI 条目... +StatusCreateRegistryEntries=正在创建注册表条目... +StatusRegisterFiles=正在注册文件... +StatusSavingUninstall=正在保存卸载信息... +StatusRunProgram=正在完成安装... +StatusRestartingApplications=正在重启应用程序... +StatusRollback=正在撤销更改... + +; *** 其它错误 +ErrorInternal2=内部错误:%1 +ErrorFunctionFailedNoCode=%1 失败 +ErrorFunctionFailed=%1 失败;错误代码 %2 +ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 +ErrorExecutingProgram=不能执行文件:%n%1 + +; *** 注册表错误 +ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 +ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 +ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 + +; *** INI 错误 +ErrorIniEntry=在文件“%1”中创建INI条目时出错。 + +; *** 文件复制错误 +FileAbortRetryIgnoreSkipNotRecommended=跳过这个文件(&S) (不推荐) +FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) +SourceIsCorrupted=源文件已损坏 +SourceDoesntExist=源文件“%1”不存在 +ExistingFileReadOnly2=无法替换现有文件,因为它是只读的。 +ExistingFileReadOnlyRetry=移除只读属性并重试(&R) +ExistingFileReadOnlyKeepExisting=保留现有文件(&K) +ErrorReadingExistingDest=尝试读取现有文件时出错: +FileExistsSelectAction=选择操作 +FileExists2=文件已经存在。 +FileExistsOverwriteExisting=覆盖已经存在的文件(&O) +FileExistsKeepExisting=保留现有的文件(&K) +FileExistsOverwriteOrKeepAll=为所有的冲突文件执行此操作(&D) +ExistingFileNewerSelectAction=选择操作 +ExistingFileNewer2=现有的文件比安装程序将要安装的文件更新。 +ExistingFileNewerOverwriteExisting=覆盖已经存在的文件(&O) +ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) +ExistingFileNewerOverwriteOrKeepAll=为所有的冲突文件执行此操作(&D) +ErrorChangingAttr=尝试改变下列现有的文件的属性时出错: +ErrorCreatingTemp=尝试在目标目录创建文件时出错: +ErrorReadingSource=尝试读取下列源文件时出错: +ErrorCopying=尝试复制下列文件时出错: +ErrorReplacingExistingFile=尝试替换现有的文件时出错: +ErrorRestartReplace=重新启动替换失败: +ErrorRenamingTemp=尝试重新命名以下目标目录中的一个文件时出错: +ErrorRegisterServer=无法注册 DLL/OCX:%1 +ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 +ErrorRegisterTypeLib=无法注册类型库:%1 + +; *** 卸载显示名字标记 +; used for example as 'My Program (32-bit)' +UninstallDisplayNameMark=%1 (%2) +; used for example as 'My Program (32-bit, All users)' +UninstallDisplayNameMarks=%1 (%2, %3) +UninstallDisplayNameMark32Bit=32位 +UninstallDisplayNameMark64Bit=64位 +UninstallDisplayNameMarkAllUsers=所有用户 +UninstallDisplayNameMarkCurrentUser=当前用户 + +; *** 安装后错误 +ErrorOpeningReadme=尝试打开自述文件时出错。 +ErrorRestartingComputer=安装程序不能重新启动电脑,请手动重启。 + +; *** 卸载消息 +UninstallNotFound=文件“%1”不存在。无法卸载。 +UninstallOpenError=文件“%1”不能打开。无法卸载。 +UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 +UninstallUnknownEntry=在卸载日志中遇到一个未知的条目 (%1) +ConfirmUninstall=您确认想要完全删除 %1 及它的所有组件吗? +UninstallOnlyOnWin64=这个安装程序只能在64位Windows中进行卸载。 +OnlyAdminCanUninstall=这个安装的程序需要有管理员权限的用户才能卸载。 +UninstallStatusLabel=正在从您的电脑中删除 %1,请稍等。 +UninstalledAll=%1 已顺利地从您的电脑中删除。 +UninstalledMost=%1 卸载完成。%n%n有一些内容无法被删除。您可以手动删除它们。 +UninstalledAndNeedsRestart=要完成 %1 的卸载,您的电脑必须重新启动。%n%n您想立即重新启动电脑吗? +UninstallDataCorrupted=文件“%1”已损坏,无法卸载 + +; *** 卸载状态消息 +ConfirmDeleteSharedFileTitle=删除共享文件吗? +ConfirmDeleteSharedFile2=系统中包含的下列共享文件已经不再被其它程序使用。您想要卸载程序删除这些共享文件吗?%n%n如果这些文件被删除,但还有程序正在使用这些文件,这些程序可能不能正确执行。如果您不能确定,选择“否”。把这些文件保留在系统中以免引起问题。 +SharedFileNameLabel=文件名: +SharedFileLocationLabel=位置: +WizardUninstalling=卸载状态 +StatusUninstalling=正在卸载 %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp=正在安装 %1。 +ShutdownBlockReasonUninstallingApp=正在卸载 %1。 + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 版本 %2 +AdditionalIcons=附加快捷方式: +CreateDesktopIcon=创建桌面快捷方式(&D) +CreateQuickLaunchIcon=创建快速运行栏快捷方式(&Q) +ProgramOnTheWeb=%1 网站 +UninstallProgram=卸载 %1 +LaunchProgram=运行 %1 +AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) +AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... +AutoStartProgramGroupDescription=启动组: +AutoStartProgram=自动启动 %1 +AddonHostProgramNotFound=%1无法找到您所选择的文件夹。%n%n您想要继续吗? diff --git a/windows/packaging/exe/make_config.yaml b/windows/packaging/exe/make_config.yaml new file mode 100644 index 0000000..8d17356 --- /dev/null +++ b/windows/packaging/exe/make_config.yaml @@ -0,0 +1,12 @@ +app_id: 728B3532-C74B-4870-9068-BE70FE12A3E6 +app_name: FlClash +publisher: chen08209 +publisher_url: https://github.com/chen08209/FlClash +display_name: FlClash +executable_name: FlClash.exe +output_base_file_name: FlClash.exe +setup_icon_file: ..\windows\runner\resources\app_icon.ico +locales: + - lang: zh + file: ..\windows\packaging\exe\ChineseSimplified.isl + - lang: en \ No newline at end of file diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..c78c115 --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. + + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..b068729 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.follow" "\0" + VALUE "FileDescription", "FlClash" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "clash" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.follow. All rights reserved." "\0" + VALUE "OriginalFilename", "FlClash.exe" "\0" + VALUE "ProductName", "clash" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..65d2a56 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..3a72d69 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"FlClash", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..3c59a7b Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..6eb617e --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,370 @@ +#include "win32_window.h" +#include "app_links/app_links_plugin_c_api.h" + +#include +#include + +#include "resource.h" + +namespace +{ + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + + /// Registry key for app theme preference. + /// + /// A value of 0 indicates apps should use dark mode. A non-zero or missing + /// value indicates apps should use light mode. + constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + + // The number of Win32Window objects that currently exist. + static int g_active_window_count = 0; + + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + + // Scale helper to convert logical scaler values to physical using passed in + // scale factor + int Scale(int source, double scale_factor) + { + return static_cast(source * scale_factor); + } + + // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. + // This API is only needed for PerMonitor V1 awareness mode. + void EnableFullDpiSupportIfAvailable(HWND hwnd) + { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) + { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) + { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); + } + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar +{ +public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar *GetInstance() + { + if (!instance_) + { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t *GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + +private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar *instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; + +const wchar_t *WindowClassRegistrar::GetWindowClass() +{ + if (!class_registered_) + { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() +{ + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() +{ + ++g_active_window_count; +} + +Win32Window::~Win32Window() +{ + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring &title, + const Point &origin, + const Size &size) +{ + + if (SendAppLinkToInstance(title)) + { + return false; + } + Destroy(); + + const wchar_t *window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) + { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() +{ + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +bool Win32Window::SendAppLinkToInstance(const std::wstring &title) +{ + // Find our exact window + HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); + + if (hwnd) + { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; + GetWindowPlacement(hwnd, &place); + + switch (place.showCmd) + { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + + // Window has been found, don't create another one. + return true; + } + + return false; +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept +{ + if (message == WM_NCCREATE) + { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } + else if (Win32Window *that = GetThisFromHandle(window)) + { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept +{ + switch (message) + { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) + { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: + { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: + { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) + { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) + { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() +{ + OnDestroy(); + + if (window_handle_) + { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) + { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept +{ + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) +{ + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() +{ + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() +{ + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) +{ + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() +{ + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() +{ + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) +{ + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) + { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..30d9573 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,107 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window +{ +public: + struct Point + { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size + { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring &title, const Point &origin, const Size &size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + +protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + +private: + friend class WindowClassRegistrar; + + bool SendAppLinkToInstance(const std::wstring &title); + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window *GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_