commit b617c95f62a7003678d756936bf42b2ed9f459da Author: problematicconsumer Date: Thu Jul 6 17:18:41 2023 +0330 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..91edff51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# 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/ + +# generated files +**/*.g.dart +**/*.freezed.dart +**/*.gen.dart +**/libclash.so +**/libclash.h +**/*.dll +**/*.dylib + +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 00000000..2e239dba --- /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: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: android + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: ios + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: linux + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: macos + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: web + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: windows + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + + # 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/Makefile b/Makefile new file mode 100644 index 00000000..046126f2 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +ANDROID_OUT=../android/app/src/main/jniLibs +NDK_BIN=$(ANDROID_HOME)/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin +GOBUILD=CGO_ENABLED=1 go build -trimpath -tags with_gvisor,with_lwip -ldflags="-w -s" -buildmode=c-shared + +get: + dart pub get +gen: + dart run build_runner build --delete-conflicting-outputs +translate: + dart run slang + +android-libs: android-x64 android-arm android-arm64 + +android-x64: + cd core && env GOOS=android GOARCH=amd64 CC=$(NDK_BIN)/x86_64-linux-android21-clang $(GOBUILD) -o $(ANDROID_OUT)/x86_64/libclash.so # building android x64 clash libs + +android-arm: + cd core && env GOOS=android GOARCH=arm GOARM=7 CC=$(NDK_BIN)/armv7a-linux-androideabi21-clang $(GOBUILD) -o $(ANDROID_OUT)/armeabi-v7a/libclash.so # building android arm clash libs + +android-arm64: + cd core && env GOOS=android GOARCH=arm64 CC=$(NDK_BIN)/aarch64-linux-android21-clang $(GOBUILD) -o $(ANDROID_OUT)/arm64-v8a/libclash.so # building android arm64 clash libs + +windows-libs: + cd core && env GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc $(GOBUILD) -o dist/libclash.dll # building windows clash libs + +release-android: get gen + flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi diff --git a/README.md b/README.md new file mode 100644 index 00000000..790b9ad5 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Hiddify Next + +

+ +

Multi-platform Proxy Frontend

+

⚠️Hiddify Next is still in early development phase⚠️

+

Windows and Android supported (more platforms coming soon)

+ +## Build from source + +### requirements: + - GO v1.19+ + - Flutter v3.10+ + - Make + - GCC + - MinGW-w64 + - Android SDK (with CMake and NDK) + + ```shell + # fetch dependencies and build generated files + $ make get gen + + # build clash native library for all supported platforms and architectures + $ make android-libs windows-libs + + # build apk for android + $ make release-android + ``` + +## Acknowledgements + - [Clash](https://github.com/Dreamacro/clash) + - [Clash Meta](https://github.com/MetaCubeX/Clash.Meta) + - [FClash](https://github.com/Fclash/Fclash) diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..af2156d2 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,21 @@ +include: package:lint/strict.yaml + +analyzer: + plugins: + - custom_lint + exclude: + - "**.g.dart" + - "lib/gen/**" + errors: + invalid_annotation_target: ignore + +linter: + rules: + sort_pub_dependencies: false + sort_unnamed_constructors_first: false + avoid_classes_with_only_static_members: false + +custom_lint: + rules: + # Enable one rule + - provider_parameters \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /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 00000000..6c5b35b6 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,74 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.hiddify.hiddify" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.hiddify.hiddify" + minSdkVersion 21 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + multiDexEnabled true + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2e05c8af --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 00000000..75bc273a Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/libtun2socks.so b/android/app/src/main/jniLibs/arm64-v8a/libtun2socks.so new file mode 100644 index 00000000..3dbdde7a Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libtun2socks.so differ diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so b/android/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so new file mode 100644 index 00000000..79e3a96a Binary files /dev/null and b/android/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so differ diff --git a/android/app/src/main/jniLibs/x86_64/libtun2socks.so b/android/app/src/main/jniLibs/x86_64/libtun2socks.so new file mode 100644 index 00000000..ce965f06 Binary files /dev/null and b/android/app/src/main/jniLibs/x86_64/libtun2socks.so differ diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/HiddifyVpnService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/HiddifyVpnService.kt new file mode 100644 index 00000000..30aecfb3 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/HiddifyVpnService.kt @@ -0,0 +1,317 @@ +package com.hiddify.hiddify + +import android.app.PendingIntent +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.ProxyInfo +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.StrictMode +import android.util.Log +import androidx.annotation.RequiresApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.File +import java.lang.ref.SoftReference + +class HiddifyVpnService : VpnService() { + companion object { + const val TAG = "Hiddify/VpnService" + const val EVENT_TAG = "Hiddify/VpnServiceEvents" + private const val TUN2SOCKS = "libtun2socks.so" + + private const val TUN_MTU = 9000 + private const val TUN_GATEWAY = "172.19.0.1" + private const val TUN_ROUTER = "172.19.0.2" + private const val TUN_SUBNET_PREFIX = 30 + private const val NET_ANY = "0.0.0.0" + private val HTTP_PROXY_LOCAL_LIST = listOf( + "localhost", + "*.local", + "127.*", + "10.*", + "172.16.*", + "172.17.*", + "172.18.*", + "172.19.*", + "172.2*", + "172.30.*", + "172.31.*", + "192.168.*" + ) + } + + private var vpnBroadcastReceiver: VpnState? = null + private var conn: ParcelFileDescriptor? = null + private lateinit var process: Process + private var isRunning = false + + // prefs + private var includeAppPackages: Set = HashSet() + + fun getService(): Service { + return this + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startVpnService() + return START_STICKY + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "creating vpn service") + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + registerBroadcastReceiver() + VpnServiceManager.vpnService = SoftReference(this) + } + + override fun onRevoke() { + Log.d(TAG, "vpn service revoked") + super.onRevoke() + stopVpnService() + } + + override fun onDestroy() { + Log.d(TAG, "vpn service destroyed") + super.onDestroy() + broadcastVpnStatus(false) + VpnServiceManager.cancelNotification() + unregisterBroadcastReceiver() + } + + private fun registerBroadcastReceiver() { + Log.d(TAG, "registering receiver in service") + vpnBroadcastReceiver = VpnState() + val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS) + registerReceiver(vpnBroadcastReceiver, intentFilter) + } + + private fun unregisterBroadcastReceiver() { + Log.d(TAG, "unregistering receiver in service") + if (vpnBroadcastReceiver != null) { + unregisterReceiver(vpnBroadcastReceiver) + vpnBroadcastReceiver = null + } + } + + private fun broadcastVpnStatus(isVpnActive: Boolean) { + Log.d(TAG, "broadcasting status= $isVpnActive") + val intent = Intent(VpnState.ACTION_VPN_STATUS) + intent.putExtra(VpnState.IS_VPN_ACTIVE, isVpnActive) + sendBroadcast(intent) + } + + @delegate:RequiresApi(Build.VERSION_CODES.P) + private val defaultNetworkRequest by lazy { + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .build() + } + + private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + + @delegate:RequiresApi(Build.VERSION_CODES.P) + private val defaultNetworkCallback by lazy { + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + setUnderlyingNetworks(arrayOf(network)) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + // it's a good idea to refresh capabilities + setUnderlyingNetworks(arrayOf(network)) + } + + override fun onLost(network: Network) { + setUnderlyingNetworks(null) + } + } + } + + private fun startVpnService() { + val prepare = prepare(this) + if (prepare != null) { + return + } + + with(Builder()) { + addAddress(TUN_GATEWAY, TUN_SUBNET_PREFIX) + setMtu(TUN_MTU) + addRoute(NET_ANY, 0) + addDnsServer(TUN_ROUTER) + allowBypass() + setBlocking(true) + setSession("Hiddify") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setMetered(false) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && VpnServiceManager.prefs.systemProxy) { + setHttpProxy( + ProxyInfo.buildDirectProxy( + "127.0.0.1", + VpnServiceManager.prefs.httpPort, + HTTP_PROXY_LOCAL_LIST, + ) + ) + } + if (includeAppPackages.isEmpty()) { + addDisallowedApplication(packageName) + } else { + includeAppPackages.forEach { + addAllowedApplication(it) + } + } + setConfigureIntent( + PendingIntent.getActivity( + this@HiddifyVpnService, + 0, + Intent().setComponent(ComponentName(packageName, "$packageName.MainActivity")), + pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT) + ) + ) + + try { + conn?.close() + } catch (ignored: Exception) { + // ignored + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) + } catch (e: Exception) { + e.printStackTrace() + } + } + try { + conn = establish() + isRunning = true + runTun2socks() + VpnServiceManager.showNotification() + Log.d(TAG, "vpn connection established") + broadcastVpnStatus(true) + } catch (e: Exception) { + Log.w(TAG, "failed to start vpn service: $e") + e.printStackTrace() + stopVpnService() + broadcastVpnStatus(false) + } + } + } + + fun stopVpnService(isForced: Boolean = true) { + Log.d(TAG, "stopping vpn service, forced: [$isForced]") + isRunning = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + connectivity.unregisterNetworkCallback(defaultNetworkCallback) + } catch (ignored: Exception) { + // ignored + } + } + + try { + Log.d(TAG, "destroying tun2socks") + process.destroy() + } catch (e: Exception) { + Log.e(TAG, e.toString()) + } + + if(isForced) { + stopSelf() + try { + conn?.close() + conn = null + } catch (ignored: Exception) { + // ignored + } + } + Log.d(TAG, "vpn service stopped") + } + + private fun runTun2socks() { + val cmd = arrayListOf( + File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, + "--netif-ipaddr", TUN_ROUTER, + "--netif-netmask", "255.255.255.252", + "--socks-server-addr", "127.0.0.1:${VpnServiceManager.prefs.socksPort}", + "--tunmtu", TUN_MTU.toString(), + "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath, + "--enable-udprelay", + "--loglevel", "notice") + + Log.d(TAG, cmd.toString()) + protect(conn!!.fd) // not sure + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(applicationContext.filesDir) + .start() + Thread(Runnable { + Log.d(TAG,"$TUN2SOCKS check") + process.waitFor() + Log.d(TAG,"$TUN2SOCKS exited") + if (isRunning) { + Log.d(packageName,"$TUN2SOCKS restart") + runTun2socks() + } + }).start() + Log.d(TAG, process.toString()) + + sendFd() + } catch (e: Exception) { + Log.d(TAG, e.toString()) + } + } + + private fun sendFd() { + val fd = conn!!.fileDescriptor + val path = File(applicationContext.filesDir, "sock_path").absolutePath + Log.d(TAG, path) + + GlobalScope.launch(Dispatchers.IO) { + var tries = 0 + while (true) try { + Thread.sleep(50L shl tries) + Log.d(TAG, "sendFd tries: $tries") + LocalSocket().use { localSocket -> + localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) + localSocket.setFileDescriptorsForSend(arrayOf(fd)) + localSocket.outputStream.write(42) + } + break + } catch (e: Exception) { + Log.d(TAG, e.toString()) + if (tries > 5) break + tries += 1 + } + } + } + + private fun pendingIntentFlags(flags: Int, mutable: Boolean = false): Int { + return if (Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT > 30 && mutable) { + flags or PendingIntent.FLAG_MUTABLE + } else { + flags or PendingIntent.FLAG_IMMUTABLE + } + } else { + flags + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt new file mode 100644 index 00000000..f0d94942 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt @@ -0,0 +1,116 @@ +package com.hiddify.hiddify + +import android.content.Intent +import android.content.IntentFilter +import android.net.VpnService +import android.util.Log +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler { + private lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private lateinit var methodResult: MethodChannel.Result + private var vpnBroadcastReceiver: VpnState? = null + + companion object { + const val VPN_PERMISSION_REQUEST_CODE = 1001 + + enum class Action(val method: String) { + GrantPermission("grant_permission"), + StartProxy("start"), + StopProxy("stop"), + RefreshStatus("refresh_status"), + SetPrefs("set_prefs") + } + } + + private fun registerBroadcastReceiver() { + Log.d(HiddifyVpnService.TAG, "registering broadcast receiver") + vpnBroadcastReceiver = VpnState() + val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS) + registerReceiver(vpnBroadcastReceiver, intentFilter) + } + + private fun unregisterBroadcastReceiver() { + Log.d(HiddifyVpnService.TAG, "unregistering broadcast receiver") + if (vpnBroadcastReceiver != null) { + unregisterReceiver(vpnBroadcastReceiver) + vpnBroadcastReceiver = null + } + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + methodChannel = + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.TAG) + methodChannel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.EVENT_TAG) + registerBroadcastReceiver() + eventChannel.setStreamHandler(vpnBroadcastReceiver) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + methodResult = result + @Suppress("UNCHECKED_CAST") + when (call.method) { + Action.GrantPermission.method -> { + grantVpnPermission() + } + + Action.StartProxy.method -> { + VpnServiceManager.startVpnService(this) + result.success(true) + } + + Action.StopProxy.method -> { + VpnServiceManager.stopVpnService() + result.success(true) + } + + Action.RefreshStatus.method -> { + val statusIntent = Intent(VpnState.ACTION_VPN_STATUS) + statusIntent.putExtra(VpnState.IS_VPN_ACTIVE, VpnServiceManager.isRunning) + sendBroadcast(statusIntent) + result.success(true) + } + + Action.SetPrefs.method -> { + val args = call.arguments as Map + VpnServiceManager.setPrefs(context, args) + result.success(true) + } + + else -> { + result.notImplemented() + } + } + } + + override fun onDestroy() { + super.onDestroy() + unregisterBroadcastReceiver() + } + + private fun grantVpnPermission() { + val vpnPermissionIntent = VpnService.prepare(this) + if (vpnPermissionIntent == null) { + onActivityResult(VPN_PERMISSION_REQUEST_CODE, RESULT_OK, null) + } else { + startActivityForResult(vpnPermissionIntent, VPN_PERMISSION_REQUEST_CODE) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == VPN_PERMISSION_REQUEST_CODE) { + methodResult.success(resultCode == RESULT_OK) + } else if (requestCode == 101010) { + methodResult.success(resultCode == RESULT_OK) + } + } +} diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/VpnServiceManager.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/VpnServiceManager.kt new file mode 100644 index 00000000..cf5afe15 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/VpnServiceManager.kt @@ -0,0 +1,97 @@ +package com.hiddify.hiddify + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Context.NOTIFICATION_SERVICE +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import java.lang.ref.SoftReference + +data class VpnServiceConfigs(val httpPort: Int = 12346, val socksPort: Int = 12347, val systemProxy: Boolean = true) + +object VpnServiceManager { + private const val NOTIFICATION_ID = 1 + private const val NOTIFICATION_CHANNEL_ID = "hiddify_vpn" + private const val NOTIFICATION_CHANNEL_NAME = "Hiddify VPN" + + var vpnService: SoftReference? = null + var prefs = VpnServiceConfigs() + var isRunning = false + + private var mBuilder: NotificationCompat.Builder? = null + private var mNotificationManager: NotificationManager? = null + + fun startVpnService(context: Context) { + val intent = Intent(context.applicationContext, HiddifyVpnService::class.java) + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stopVpnService() { + val service = vpnService?.get() ?: return + service.stopVpnService() + } + + fun setPrefs(context: Context,args: Map) { + prefs = prefs.copy( + httpPort = args["httpPort"] as Int? ?: prefs.httpPort, + socksPort = args["socksPort"] as Int? ?: prefs.socksPort, + systemProxy = args["systemProxy"] as Boolean? ?: prefs.systemProxy, + ) + if(isRunning) { + stopVpnService() + startVpnService(context) + } + } + + fun showNotification() { + val service = vpnService?.get()?.getService() ?: return + val channelId = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + } else { + "" + } + + mBuilder = NotificationCompat.Builder(service, channelId) + .setSmallIcon(R.drawable.ic_stat_logo) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setOngoing(true) + .setShowWhen(false) + .setOnlyAlertOnce(true) + .setContentTitle("Hiddify") + .setContentText("Connected") + + service.startForeground(NOTIFICATION_ID, mBuilder?.build()) + } + + fun cancelNotification() { + val service = vpnService?.get()?.getService() ?: return + service.stopForeground(true) + mBuilder = null + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(): String { + val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH) + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + getNotificationManager()?.createNotificationChannel( + channel + ) + return NOTIFICATION_CHANNEL_ID + } + + private fun getNotificationManager(): NotificationManager? { + if (mNotificationManager == null) { + val service = vpnService?.get()?.getService() ?: return null + mNotificationManager = service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + } + return mNotificationManager + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/VpnState.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/VpnState.kt new file mode 100644 index 00000000..0e1e41e2 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/VpnState.kt @@ -0,0 +1,34 @@ +package com.hiddify.hiddify + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import io.flutter.plugin.common.EventChannel + +class VpnState : BroadcastReceiver(), EventChannel.StreamHandler{ + companion object { + const val ACTION_VPN_STATUS = "Hiddify.VpnState.ACTION_VPN_STATUS" + const val IS_VPN_ACTIVE = "isVpnActive" + } + + + private var eventSink: EventChannel.EventSink? = null + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ACTION_VPN_STATUS) { + val isVpnActive = intent.getBooleanExtra(IS_VPN_ACTIVE, false) + Log.d(HiddifyVpnService.TAG, "send to flutter: status= $isVpnActive") + VpnServiceManager.isRunning = isVpnActive + eventSink?.success(isVpnActive) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 00000000..6054d669 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png new file mode 100644 index 00000000..75cbdc7d Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 00000000..1c32323b Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 00000000..9abf1640 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png new file mode 100644 index 00000000..efe228a1 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 00000000..df97d503 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png new file mode 100644 index 00000000..6054d669 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 00000000..9abf1640 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png new file mode 100644 index 00000000..6c8bac85 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png new file mode 100644 index 00000000..07b7c812 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 00000000..d1fb7aff Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png new file mode 100644 index 00000000..31264850 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 00000000..5b96cbfb Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png new file mode 100644 index 00000000..6c9351d0 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 00000000..3107d37f Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..3cc4948a --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 00000000..6c8bac85 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 00000000..ceac52d7 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 00000000..d1fb7aff Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 00000000..61708b85 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 00000000..5b96cbfb Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 00000000..77db48d3 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 00000000..3107d37f Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..3cc4948a --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + 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 00000000..e7b046de --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + 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 00000000..e7b046de --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..0fd43549 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..767e978e Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a5782466 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000..b3f4de25 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..2538c3cc Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..8b8d451b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..2a0e45a3 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..4748b05a Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000..e4ff5412 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..804816b6 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..af49036d Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..94988fba Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a5db8095 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000..305a6407 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..ca78da3a Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..4a0b83ce Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..37ea66b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..34a0e622 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000..b61e330a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1ec3d3a7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..27472c28 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..a140fc39 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..dc9a5cb2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 00000000..5d76a6e0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..4e695273 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 00000000..ba4b2aee --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + 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 00000000..dbc9ea9f --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..e437c387 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..72bd02f5 --- /dev/null +++ b/android/app/src/main/res/values/arrays.xml @@ -0,0 +1,80 @@ + + + + + 1.0.0.0/8 + 2.0.0.0/7 + 4.0.0.0/6 + 8.0.0.0/7 + 11.0.0.0/8 + 12.0.0.0/6 + 16.0.0.0/4 + 32.0.0.0/3 + 64.0.0.0/3 + 96.0.0.0/4 + 112.0.0.0/5 + 120.0.0.0/6 + 124.0.0.0/7 + 126.0.0.0/8 + 128.0.0.0/3 + 160.0.0.0/5 + 168.0.0.0/8 + 169.0.0.0/9 + 169.128.0.0/10 + 169.192.0.0/11 + 169.224.0.0/12 + 169.240.0.0/13 + 169.248.0.0/14 + 169.252.0.0/15 + 169.255.0.0/16 + 170.0.0.0/7 + 172.0.0.0/12 + 172.32.0.0/11 + 172.64.0.0/10 + 172.128.0.0/9 + 173.0.0.0/8 + 174.0.0.0/7 + 176.0.0.0/4 + 192.0.0.0/9 + 192.128.0.0/11 + 192.160.0.0/13 + 192.169.0.0/16 + 192.170.0.0/15 + 192.172.0.0/14 + 192.176.0.0/12 + 192.192.0.0/10 + 193.0.0.0/8 + 194.0.0.0/7 + 196.0.0.0/6 + 200.0.0.0/5 + 208.0.0.0/4 + 240.0.0.0/5 + 248.0.0.0/6 + 252.0.0.0/7 + 254.0.0.0/8 + 255.0.0.0/9 + 255.128.0.0/10 + 255.192.0.0/11 + 255.224.0.0/12 + 255.240.0.0/13 + 255.248.0.0/14 + 255.252.0.0/15 + 255.254.0.0/16 + 255.255.0.0/17 + 255.255.128.0/18 + 255.255.192.0/19 + 255.255.224.0/20 + 255.255.240.0/21 + 255.255.248.0/22 + 255.255.252.0/23 + 255.255.254.0/24 + 255.255.255.0/25 + 255.255.255.128/26 + 255.255.255.192/27 + 255.255.255.224/28 + 255.255.255.240/29 + 255.255.255.248/30 + 255.255.255.252/31 + 255.255.255.254/32 + + \ No newline at end of file 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 00000000..0d1fa8fc --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /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 00000000..f7eb7f63 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + 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 00000000..94adc3a3 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3c472b99 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/core/clash/Country.mmdb b/assets/core/clash/Country.mmdb new file mode 100644 index 00000000..b8795f39 Binary files /dev/null and b/assets/core/clash/Country.mmdb differ diff --git a/assets/core/clash/config.yaml b/assets/core/clash/config.yaml new file mode 100644 index 00000000..f70a5e7a --- /dev/null +++ b/assets/core/clash/config.yaml @@ -0,0 +1,16 @@ +external-controller: 127.0.0.1:22345 + +mixed-port: 22346 + +rules: + # localhost rule + - DOMAIN-KEYWORD,announce,DIRECT + - DOMAIN-KEYWORD,torrent,DIRECT + - DOMAIN-KEYWORD,tracker,DIRECT + - DOMAIN-SUFFIX,smtp,DIRECT + - DOMAIN-SUFFIX,local,DIRECT + - IP-CIDR,192.168.0.0/16,DIRECT + - IP-CIDR,10.0.0.0/8,DIRECT + - IP-CIDR,172.16.0.0/12,DIRECT + - IP-CIDR,127.0.0.0/8,DIRECT + - IP-CIDR,100.64.0.0/10,DIRECT \ No newline at end of file diff --git a/assets/images/ic_launcher.png b/assets/images/ic_launcher.png new file mode 100644 index 00000000..0a232f76 Binary files /dev/null and b/assets/images/ic_launcher.png differ diff --git a/assets/images/ic_launcher_background.png b/assets/images/ic_launcher_background.png new file mode 100644 index 00000000..4723fff0 Binary files /dev/null and b/assets/images/ic_launcher_background.png differ diff --git a/assets/images/ic_launcher_foreground.png b/assets/images/ic_launcher_foreground.png new file mode 100644 index 00000000..4fa2cfb5 Binary files /dev/null and b/assets/images/ic_launcher_foreground.png differ diff --git a/assets/images/ic_launcher_monochrome.png b/assets/images/ic_launcher_monochrome.png new file mode 100644 index 00000000..3e6ec17b Binary files /dev/null and b/assets/images/ic_launcher_monochrome.png differ diff --git a/assets/images/ic_launcher_round.png b/assets/images/ic_launcher_round.png new file mode 100644 index 00000000..6ed06635 Binary files /dev/null and b/assets/images/ic_launcher_round.png differ diff --git a/assets/images/ic_notification_icon.png b/assets/images/ic_notification_icon.png new file mode 100644 index 00000000..8cc24d7c Binary files /dev/null and b/assets/images/ic_notification_icon.png differ diff --git a/assets/images/logo.svg b/assets/images/logo.svg new file mode 100644 index 00000000..7232f9b7 --- /dev/null +++ b/assets/images/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/logo_round.ico b/assets/images/logo_round.ico new file mode 100644 index 00000000..5e470af5 Binary files /dev/null and b/assets/images/logo_round.ico differ diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json new file mode 100644 index 00000000..fa56448d --- /dev/null +++ b/assets/translations/strings.i18n.json @@ -0,0 +1,125 @@ +{ + "general": { + "appTitle": "hiddify", + "reset": "reset", + "toggle": { + "enabled": "enabled", + "disabled": "disabled" + } + }, + "home": { + "pageTitle": "home", + "emptyProfilesMsg": "start by adding subscription profile", + "noActiveProfileMsg": "select a profile", + "connection": { + "tapToConnect": "tap to connect", + "connecting": "connecting", + "disconnecting": "disconnecting", + "connected": "connected" + } + }, + "profile": { + "overviewPageTitle": "profiles", + "detailsPageTitle": "profile", + "subscription": { + "traffic": "traffic", + "updatedTimeAgo": "updated ${timeago}", + "remaining": "remaining", + "expired": "expired", + "noTraffic": "no traffic" + }, + "add": { + "buttonText": "add new profile", + "fromClipboard": "add from clipboard", + "scanQr": "Scan QR code", + "manually": "add manually", + "invalidUrlMsg": "unexpected url" + }, + "update": { + "failureMsg": "failed to update profile: ${reason}", + "successMsg": "successfully updated profile" + }, + "delete": { + "buttonText": "delete", + "confirmationMsg": "delete profile for ever? this can not be undone", + "successMsg": "successfully deleted profile" + }, + "save": { + "buttonText": "save", + "successMsg": "successfully saved profile" + }, + "detailsForm": { + "nameHint": "name", + "urlHint": "url", + "emptyNameMsg": "name can't be empty", + "invalidUrlMsg": "invalid URL" + } + }, + "proxies": { + "pageTitle": "proxies", + "emptyProxiesMsg": "no proxies", + "delayTestTooltip": "delay test", + "cancelTestButtonText": "cancel test" + }, + "logs": { + "pageTitle": "logs", + "clearLogsButtonText": "clear", + "filterHint": "filter", + "allLevelsFilter": "all" + }, + "settings": { + "pageTitle": "settings", + "appearance": { + "sectionTitle": "appearance", + "themeMode": "theme mode", + "themeModes": { + "system": "follow system theme", + "dark": "dark mode", + "light": "light mode" + }, + "trueBlack": "true black" + }, + "network": { + "sectionTitle": "network", + "systemProxy": "system proxy", + "systemProxyMsg": "Attach http proxy to VpnService", + "bypassPrivateNetworks": "bypass private networks", + "bypassPrivateNetworksMsg": "Bypass private network addresses" + }, + "clash": { + "sectionTitle": "clash proxy overrides", + "doNotModify": "do not modify", + "overrides": { + "httpPort": "HTTP Port", + "socksPort": "Socks Port", + "redirPort": "Redirect Port", + "tproxyPort": "TProxy Port", + "mixedPort": "Mixed Port", + "allowLan": "Allow LAN", + "ipv6": "IPv6", + "mode": "Mode", + "logLevel": "Log Level" + } + } + }, + "tray": { + "dashboard": "dashboard", + "quit": "quit", + "systemProxy": "system proxy" + }, + "failure": { + "unexpected": "unexpected failure", + "clash": { + "unexpected": "unexpected failure", + "core": "clash failure ${reason}" + }, + "connectivity": { + "unexpected": "unexpected failure" + }, + "profiles": { + "unexpected": "unexpected failure", + "notFound": "profile not found", + "invalidConfig": "invalid config" + } + } +} \ No newline at end of file diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..b33aa2b2 --- /dev/null +++ b/build.yaml @@ -0,0 +1,17 @@ +targets: + $default: + builders: + drift_dev: + generate_for: + - "lib/data/local/**" + options: + store_date_time_values_as_text: true + slang_build_runner: + options: + base_locale: en + input_directory: assets/translations + input_file_pattern: .i18n.json + output_directory: lib/gen + output_file_name: translations.g.dart + translation_class_visibility: public + locale_handling: false diff --git a/core/bridge/dart_api/BUILD.gn b/core/bridge/dart_api/BUILD.gn new file mode 100644 index 00000000..2b10262f --- /dev/null +++ b/core/bridge/dart_api/BUILD.gn @@ -0,0 +1,23 @@ +# Copyright (c) 2021, 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. + +import("../../sdk_args.gni") + +# This rule copies header files to include/ +copy("copy_headers") { + visibility = [ "../../sdk:copy_headers" ] + + sources = [ + "dart_api.h", + "dart_api_dl.c", + "dart_api_dl.h", + "dart_native_api.h", + "dart_tools_api.h", + "dart_version.h", + "internal/dart_api_dl_impl.h", + ] + + outputs = + [ "$root_out_dir/$dart_sdk_output/include/{{source_target_relative}}" ] +} diff --git a/core/bridge/dart_api/analyze_snapshot_api.h b/core/bridge/dart_api/analyze_snapshot_api.h new file mode 100644 index 00000000..0e68d5cc --- /dev/null +++ b/core/bridge/dart_api/analyze_snapshot_api.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, 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_ANALYZE_SNAPSHOT_API_H_ +#define RUNTIME_INCLUDE_ANALYZE_SNAPSHOT_API_H_ + +#include + +namespace dart { +namespace snapshot_analyzer { +typedef struct { + const uint8_t* vm_snapshot_data; + const uint8_t* vm_snapshot_instructions; + const uint8_t* vm_isolate_data; + const uint8_t* vm_isolate_instructions; +} Dart_SnapshotAnalyzerInformation; + +void Dart_DumpSnapshotInformationAsJson(char** buffer, + intptr_t* buffer_length, + Dart_SnapshotAnalyzerInformation* info); + +void Dart_DumpSnapshotInformationPP(Dart_SnapshotAnalyzerInformation* info); + +} // namespace snapshot_analyzer +} // namespace dart + +#endif // RUNTIME_INCLUDE_ANALYZE_SNAPSHOT_API_H_ diff --git a/core/bridge/dart_api/bin/dart_io_api.h b/core/bridge/dart_api/bin/dart_io_api.h new file mode 100644 index 00000000..cc647976 --- /dev/null +++ b/core/bridge/dart_api/bin/dart_io_api.h @@ -0,0 +1,69 @@ +// Copyright (c) 2015, 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_BIN_DART_IO_API_H_ +#define RUNTIME_INCLUDE_BIN_DART_IO_API_H_ + +#include "dart_tools_api.h" + +namespace dart { +namespace bin { + +// Bootstraps 'dart:io'. +void BootstrapDartIo(); + +// Cleans up 'dart:io'. +void CleanupDartIo(); + +// Lets dart:io know where the system temporary directory is located. +// Currently only wired up on Android. +void SetSystemTempDirectory(const char* system_temp); + +// Tells the system whether to capture Stdout events. +void SetCaptureStdout(bool value); + +// Tells the system whether to capture Stderr events. +void SetCaptureStderr(bool value); + +// Should Stdout events be captured? +bool ShouldCaptureStdout(); + +// Should Stderr events be captured? +bool ShouldCaptureStderr(); + +// Set the executable name used by Platform.executable. +void SetExecutableName(const char* executable_name); + +// Set the arguments used by Platform.executableArguments. +void SetExecutableArguments(int script_index, char** argv); + +// Set dart:io implementation specific fields of Dart_EmbedderInformation. +void GetIOEmbedderInformation(Dart_EmbedderInformation* info); + +// Appropriate to assign to Dart_InitializeParams.file_open/read/write/close. +void* OpenFile(const char* name, bool write); +void ReadFile(uint8_t** data, intptr_t* file_len, void* stream); +void WriteFile(const void* buffer, intptr_t num_bytes, void* stream); +void CloseFile(void* stream); + +// Generates 'length' random bytes into 'buffer'. Returns true on success +// and false on failure. This is appropriate to assign to +// Dart_InitializeParams.entropy_source. +bool GetEntropy(uint8_t* buffer, intptr_t length); + +// Performs a lookup of the I/O Dart_NativeFunction with a specified 'name' and +// 'argument_count'. Returns NULL if no I/O native function with a matching +// name and parameter count is found. +Dart_NativeFunction LookupIONative(Dart_Handle name, + int argument_count, + bool* auto_setup_scope); + +// Returns the symbol for I/O native function 'nf'. Returns NULL if 'nf' is not +// a valid I/O native function. +const uint8_t* LookupIONativeSymbol(Dart_NativeFunction nf); + +} // namespace bin +} // namespace dart + +#endif // RUNTIME_INCLUDE_BIN_DART_IO_API_H_ diff --git a/core/bridge/dart_api/dart_api.h b/core/bridge/dart_api/dart_api.h new file mode 100644 index 00000000..6e88083e --- /dev/null +++ b/core/bridge/dart_api/dart_api.h @@ -0,0 +1,4172 @@ +/* + * 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 + +#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 (0x00000007) + +/** 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; +} 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(); + +/** + * 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(); + +/* + * Stops the heap sampling profiler for each thread in the VM. + */ +DART_EXPORT void Dart_DisableHeapSampling(); + +/* 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/bridge/dart_api/dart_api_dl.c b/core/bridge/dart_api/dart_api_dl.c new file mode 100644 index 00000000..c4a68f44 --- /dev/null +++ b/core/bridge/dart_api/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/bridge/dart_api/dart_api_dl.h b/core/bridge/dart_api/dart_api_dl.h new file mode 100644 index 00000000..cce34500 --- /dev/null +++ b/core/bridge/dart_api/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/bridge/dart_api/dart_embedder_api.h b/core/bridge/dart_api/dart_embedder_api.h new file mode 100644 index 00000000..e565ebf6 --- /dev/null +++ b/core/bridge/dart_api/dart_embedder_api.h @@ -0,0 +1,108 @@ +// Copyright (c) 2018, 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_EMBEDDER_API_H_ +#define RUNTIME_INCLUDE_DART_EMBEDDER_API_H_ + +#include "include/dart_api.h" +#include "include/dart_tools_api.h" + +namespace dart { +namespace embedder { + +// Initialize all subsystems of the embedder. +// +// Must be called before the `Dart_Initialize()` call to initialize the +// Dart VM. +// +// Returns true on success and false otherwise, in which case error would +// contain error message. +DART_WARN_UNUSED_RESULT bool InitOnce(char** error); + +// Cleans up all subsystems of the embedder. +// +// Must be called after the `Dart_Cleanup()` call to initialize the +// Dart VM. +void Cleanup(); + +// Common arguments that are passed to isolate creation callback and to +// API methods that create isolates. +struct IsolateCreationData { + // URI for the main script that will be running in the isolate. + const char* script_uri; + + // Advisory name of the main method that will be run by isolate. + // Only used for error messages. + const char* main; + + // Isolate creation flags. Might be absent. + Dart_IsolateFlags* flags; + + // Isolate group callback data. + void* isolate_group_data; + + // Isolate callback data. + void* isolate_data; +}; + +// Create and initialize kernel-service isolate. This method should be used +// when VM invokes isolate creation callback with DART_KERNEL_ISOLATE_NAME as +// script_uri. +// The isolate is created from the given snapshot (might be kernel data or +// app-jit snapshot). +DART_WARN_UNUSED_RESULT Dart_Isolate +CreateKernelServiceIsolate(const IsolateCreationData& data, + const uint8_t* buffer, + intptr_t buffer_size, + char** error); + +// Service isolate configuration. +struct VmServiceConfiguration { + enum { + kBindHttpServerToAFreePort = 0, + kDoNotAutoStartHttpServer = -1 + }; + + // Address to which HTTP server will be bound. + const char* ip; + + // Default port. See enum above for special values. + int port; + + // If non-null, connection information for the VM service will be output to a + // file in JSON format at the location specified. + const char* write_service_info_filename; + + // TODO(vegorov) document these ones. + bool dev_mode; + bool deterministic; + bool disable_auth_codes; +}; + +// Create and initialize vm-service isolate from the given AOT snapshot, which +// is expected to contain all necessary 'vm-service' libraries. +// This method should be used when VM invokes isolate creation callback with +// DART_VM_SERVICE_ISOLATE_NAME as script_uri. +DART_WARN_UNUSED_RESULT Dart_Isolate +CreateVmServiceIsolate(const IsolateCreationData& data, + const VmServiceConfiguration& config, + const uint8_t* isolate_data, + const uint8_t* isolate_instr, + char** error); + +// Create and initialize vm-service isolate from the given kernel binary, which +// is expected to contain all necessary 'vm-service' libraries. +// This method should be used when VM invokes isolate creation callback with +// DART_VM_SERVICE_ISOLATE_NAME as script_uri. +DART_WARN_UNUSED_RESULT Dart_Isolate +CreateVmServiceIsolateFromKernel(const IsolateCreationData& data, + const VmServiceConfiguration& config, + const uint8_t* kernel_buffer, + intptr_t kernel_buffer_size, + char** error); + +} // namespace embedder +} // namespace dart + +#endif // RUNTIME_INCLUDE_DART_EMBEDDER_API_H_ diff --git a/core/bridge/dart_api/dart_native_api.h b/core/bridge/dart_api/dart_native_api.h new file mode 100644 index 00000000..79194e03 --- /dev/null +++ b/core/bridge/dart_api/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/bridge/dart_api/dart_tools_api.h b/core/bridge/dart_api/dart_tools_api.h new file mode 100644 index 00000000..aab2fa54 --- /dev/null +++ b/core/bridge/dart_api/dart_tools_api.h @@ -0,0 +1,588 @@ +// 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. + * + * \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_async_id The second timestamp of the event or + * the async id. + * \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_async_id, + 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. */ + int64_t timestamp1_or_async_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/bridge/dart_api/dart_version.h b/core/bridge/dart_api/dart_version.h new file mode 100644 index 00000000..e2d3651f --- /dev/null +++ b/core/bridge/dart_api/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/bridge/dart_api/internal/dart_api_dl_impl.h b/core/bridge/dart_api/internal/dart_api_dl_impl.h new file mode 100644 index 00000000..e4a56893 --- /dev/null +++ b/core/bridge/dart_api/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/bridge/go.mod b/core/bridge/go.mod new file mode 100644 index 00000000..25a69321 --- /dev/null +++ b/core/bridge/go.mod @@ -0,0 +1,3 @@ +module hiddify.com/hiddify/bridge + +go 1.20 diff --git a/core/bridge/lib.go b/core/bridge/lib.go new file mode 100644 index 00000000..32ab1723 --- /dev/null +++ b/core/bridge/lib.go @@ -0,0 +1,60 @@ +package bridge + +/* +#include "stdint.h" +#include "dart_api/dart_api_dl.h" +#include "dart_api/dart_api_dl.c" +#include "dart_api/dart_native_api.h" +// Go does not allow calling C function pointers directly. +// we mock a function to call Dart_PostCObject_DL +bool GoDart_PostCObject(Dart_Port_DL port, Dart_CObject* obj) { + return Dart_PostCObject_DL(port, obj); +} +*/ +import "C" +import ( + "encoding/json" + "fmt" + "unsafe" +) + +type DartResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data string `json:"data"` +} + +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 SendResponseToPort(port int64, response *DartResponse) { + var obj C.Dart_CObject + obj._type = C.Dart_CObject_kString + responseJson, _ := json.Marshal(response) + msg_obj := C.CString(string(responseJson)) // go string -> char*s + // union type, we do a force convertion + ptr := unsafe.Pointer(&obj.value[0]) + *(**C.char)(ptr) = msg_obj + ret := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) + if !ret { + fmt.Println("ERROR: post to port ", port, " failed", responseJson) + } +} + +func SendStringToPort(port int64, msg string) { + var obj C.Dart_CObject + obj._type = C.Dart_CObject_kString + msg_obj := C.CString(msg) // go string -> char*s + // union type, we do a force convertion + ptr := unsafe.Pointer(&obj.value[0]) + *(**C.char)(ptr) = msg_obj + ret := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) + if !ret { + fmt.Println("ERROR: post to port ", port, " failed", msg) + } +} diff --git a/core/configs.go b/core/configs.go new file mode 100644 index 00000000..422cf200 --- /dev/null +++ b/core/configs.go @@ -0,0 +1,292 @@ +package main + +import "C" +import ( + "encoding/json" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + CO "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub/executor" + P "github.com/Dreamacro/clash/listener" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel" + bridge "hiddify.com/hiddify/bridge" +) + +type configSchema struct { + Port *int `json:"port"` + SocksPort *int `json:"socks-port"` + RedirPort *int `json:"redir-port"` + TProxyPort *int `json:"tproxy-port"` + MixedPort *int `json:"mixed-port"` + Tun *tunSchema `json:"tun"` + TuicServer *tuicServerSchema `json:"tuic-server"` + ShadowSocksConfig *string `json:"ss-config"` + VmessConfig *string `json:"vmess-config"` + TcptunConfig *string `json:"tcptun-config"` + UdptunConfig *string `json:"udptun-config"` + AllowLan *bool `json:"allow-lan"` + BindAddress *string `json:"bind-address"` + Mode *tunnel.TunnelMode `json:"mode"` + LogLevel *log.LogLevel `json:"log-level"` + IPv6 *bool `json:"ipv6"` + Sniffing *bool `json:"sniffing"` + TcpConcurrent *bool `json:"tcp-concurrent"` + InterfaceName *string `json:"interface-name"` +} + +type tunSchema struct { + Enable bool `yaml:"enable" json:"enable"` + Device *string `yaml:"device" json:"device"` + Stack *CO.TUNStack `yaml:"stack" json:"stack"` + DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` + AutoRoute *bool `yaml:"auto-route" json:"auto-route"` + AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` + //RedirectToTun []string `yaml:"-" json:"-"` + + MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` + //Inet4Address *[]config.ListenPrefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address *[]LC.ListenPrefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` + Inet4RouteAddress *[]LC.ListenPrefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` + Inet6RouteAddress *[]LC.ListenPrefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` + IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` +} + +type tuicServerSchema struct { + Enable bool `yaml:"enable" json:"enable"` + Listen *string `yaml:"listen" json:"listen"` + Token *[]string `yaml:"token" json:"token"` + Users *map[string]string `yaml:"users" json:"users,omitempty"` + Certificate *string `yaml:"certificate" json:"certificate"` + PrivateKey *string `yaml:"private-key" json:"private-key"` + CongestionController *string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` + MaxIdleTime *int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` + AuthenticationTimeout *int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` + ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"` + MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` + CWND *int `yaml:"cwnd" json:"cwnd,omitempty"` +} + +func pointerOrDefault(p *int, def int) int { + if p != nil { + return *p + } + return def +} + +func pointerOrDefaultString(p *string, def string) string { + if p != nil { + return *p + } + + return def +} + +func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { + if p != nil { + def.Enable = p.Enable + if p.Device != nil { + def.Device = *p.Device + } + if p.Stack != nil { + def.Stack = *p.Stack + } + if p.DNSHijack != nil { + def.DNSHijack = *p.DNSHijack + } + if p.AutoRoute != nil { + def.AutoRoute = *p.AutoRoute + } + if p.AutoDetectInterface != nil { + def.AutoDetectInterface = *p.AutoDetectInterface + } + if p.MTU != nil { + def.MTU = *p.MTU + } + //if p.Inet4Address != nil { + // def.Inet4Address = *p.Inet4Address + //} + if p.Inet6Address != nil { + def.Inet6Address = *p.Inet6Address + } + if p.IncludeUID != nil { + def.IncludeUID = *p.IncludeUID + } + if p.IncludeUIDRange != nil { + def.IncludeUIDRange = *p.IncludeUIDRange + } + if p.ExcludeUID != nil { + def.ExcludeUID = *p.ExcludeUID + } + if p.ExcludeUIDRange != nil { + def.ExcludeUIDRange = *p.ExcludeUIDRange + } + if p.IncludeAndroidUser != nil { + def.IncludeAndroidUser = *p.IncludeAndroidUser + } + if p.IncludePackage != nil { + def.IncludePackage = *p.IncludePackage + } + if p.ExcludePackage != nil { + def.ExcludePackage = *p.ExcludePackage + } + if p.EndpointIndependentNat != nil { + def.EndpointIndependentNat = *p.EndpointIndependentNat + } + if p.UDPTimeout != nil { + def.UDPTimeout = *p.UDPTimeout + } + if p.FileDescriptor != nil { + def.FileDescriptor = *p.FileDescriptor + } + } + return def +} + +func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicServer { + if p != nil { + def.Enable = p.Enable + if p.Listen != nil { + def.Listen = *p.Listen + } + if p.Token != nil { + def.Token = *p.Token + } + if p.Users != nil { + def.Users = *p.Users + } + if p.Certificate != nil { + def.Certificate = *p.Certificate + } + if p.PrivateKey != nil { + def.PrivateKey = *p.PrivateKey + } + if p.CongestionController != nil { + def.CongestionController = *p.CongestionController + } + if p.MaxIdleTime != nil { + def.MaxIdleTime = *p.MaxIdleTime + } + if p.AuthenticationTimeout != nil { + def.AuthenticationTimeout = *p.AuthenticationTimeout + } + if p.ALPN != nil { + def.ALPN = *p.ALPN + } + if p.MaxUdpRelayPacketSize != nil { + def.MaxUdpRelayPacketSize = *p.MaxUdpRelayPacketSize + } + if p.CWND != nil { + def.CWND = *p.CWND + } + } + return def +} + +//export getConfigs +func getConfigs(port C.longlong) { + general := executor.GetGeneral() + data, err := json.Marshal(general) + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true, Data: string(data)}) +} + +//export patchConfigs +func patchConfigs(port C.longlong, patchStr *C.char) { + patch := C.GoString(patchStr) + general := &configSchema{} + if err := json.Unmarshal([]byte(patch), general); err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + + if general.AllowLan != nil { + P.SetAllowLan(*general.AllowLan) + } + + if general.BindAddress != nil { + P.SetBindAddress(*general.BindAddress) + } + + if general.Sniffing != nil { + tunnel.SetSniffing(*general.Sniffing) + } + + if general.TcpConcurrent != nil { + dialer.SetTcpConcurrent(*general.TcpConcurrent) + } + + if general.InterfaceName != nil { + dialer.DefaultInterface.Store(*general.InterfaceName) + } + + ports := P.GetPorts() + + tcpIn := tunnel.TCPIn() + udpIn := tunnel.UDPIn() + natTable := tunnel.NatTable() + + P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port), tcpIn) + P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort), tcpIn, udpIn) + P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn, natTable) + P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn, natTable) + P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) + P.ReCreateTun(pointerOrDefaultTun(general.Tun, P.LastTunConf), tcpIn, udpIn) + P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn) + P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn) + P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tcpIn, udpIn) + + if general.Mode != nil { + tunnel.SetMode(*general.Mode) + } + + if general.LogLevel != nil { + log.SetLevel(*general.LogLevel) + } + + if general.IPv6 != nil { + resolver.DisableIPv6 = !*general.IPv6 + } + + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true}) +} + +//export updateConfigs +func updateConfigs(port C.longlong, pathStr *C.char, force bool) { + go func() { + path := C.GoString(pathStr) + cfg, err := executor.ParseWithPath(path) + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + executor.ApplyConfig(cfg, force) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true}) + }() +} + +//export validateConfig +func validateConfig(port C.longlong, path *C.char) { + go func() { + if _, err := executor.ParseWithPath(C.GoString(path)); err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true, Data: string("true")}) + }() +} diff --git a/core/go.mod b/core/go.mod new file mode 100644 index 00000000..b256f8c7 --- /dev/null +++ b/core/go.mod @@ -0,0 +1,110 @@ +module hiddify.com/hiddify/clash + +go 1.20 + +require ( + github.com/Dreamacro/clash v1.16.0 + hiddify.com/hiddify/bridge v1.0.0 +) + +require ( + github.com/3andne/restls-client-go v0.1.4 // indirect + github.com/RyuaNerin/go-krypto v1.0.2 // indirect + github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cilium/ebpf v0.10.0 // indirect + github.com/coreos/go-iptables v0.6.0 // indirect + github.com/dlclark/regexp2 v1.10.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.6.0 // indirect + github.com/go-chi/chi/v5 v5.0.8 // indirect + github.com/go-chi/cors v1.2.1 // indirect + github.com/go-chi/render v1.0.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // 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-20230611153922-78842f086475 // indirect + github.com/metacubex/quic-go v0.35.2-0.20230603072621-ea2663348ebb // indirect + github.com/metacubex/sing-shadowsocks v0.2.2 // indirect + github.com/metacubex/sing-shadowsocks2 v0.1.0 // indirect + github.com/metacubex/sing-tun v0.1.5-0.20230618235243-65051e73b018 // indirect + github.com/metacubex/sing-vmess v0.1.5 // indirect + github.com/metacubex/sing-wireguard v0.0.0-20230611155257-1498ae315a28 // indirect + github.com/miekg/dns v1.1.54 // indirect + github.com/mroth/weightedrand/v2 v2.0.1 // indirect + github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect + github.com/openacid/low v0.1.21 // indirect + github.com/oschwald/geoip2-golang v1.8.0 // indirect + github.com/oschwald/maxminddb-golang v1.10.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.2 // indirect + github.com/quic-go/qtls-go1-20 v0.2.2 // indirect + github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect + github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect + github.com/sagernet/sing v0.2.5 // indirect + github.com/sagernet/sing-mux v0.1.0 // indirect + github.com/sagernet/sing-shadowtls v0.1.2 // indirect + github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect + github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 // indirect + github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect + github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect + github.com/shirou/gopsutil/v3 v3.23.5 // 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.2 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/xtls/go v0.0.0-20230107031059-4610f88d00f3 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + github.com/zhangyunhao116/fastrand v0.3.0 // indirect + gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect + go.etcd.io/bbolt v1.3.7 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.11.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.6.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.2.1 // indirect +) + +replace hiddify.com/hiddify/bridge => ./bridge + +replace github.com/Dreamacro/clash => github.com/MetaCubeX/Clash.Meta v1.15.0 + +replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20230530121223-b768faae5c6b diff --git a/core/go.sum b/core/go.sum new file mode 100644 index 00000000..b4c25762 --- /dev/null +++ b/core/go.sum @@ -0,0 +1,276 @@ +github.com/3andne/restls-client-go v0.1.4 h1:kLNC2aSRHPlEVYmTj6EOqJoorCpobEe2toMRSfBF7FU= +github.com/3andne/restls-client-go v0.1.4/go.mod h1:04CGbRk1BwBiEDles8b5mlKgTqIwE5MqF7JDloJV47I= +github.com/MetaCubeX/Clash.Meta v1.15.0 h1:Ugyq8Zb0bvyYm2Fx6/YgZpZDzaRaic+7h0p3hkzOatQ= +github.com/MetaCubeX/Clash.Meta v1.15.0/go.mod h1:4ngBL0k/aH6OFIWK3SH+1n753KoE7W2X2c9gn72eIpY= +github.com/RyuaNerin/go-krypto v1.0.2 h1:9KiZrrBs+tDrQ66dNy4nrX6SzntKtSKdm0wKHhdB4WM= +github.com/RyuaNerin/go-krypto v1.0.2/go.mod h1:17LzMeJCgzGTkPH3TmfzRnEJ/yA7ErhTPp9sxIqONtA= +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/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +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.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ= +github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE= +github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= +github.com/coreos/go-iptables v0.6.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.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.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/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.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= +github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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-20230516061539-49801966e6cb h1:6fDKEAXwe3rsfS4khW3EZ8kEqmSiV9szhMPcDrD+Y7Q= +github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4= +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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +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/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-20230611153922-78842f086475 h1:qSEOvPPaMrWggFyFhFYGyMR8i1HKyhXjdi1QYUAa2ww= +github.com/metacubex/gvisor v0.0.0-20230611153922-78842f086475/go.mod h1:wehEpqiogdeyncfhckJP5gD2LtBgJW0wnDC24mJ+8Jg= +github.com/metacubex/quic-go v0.35.2-0.20230603072621-ea2663348ebb h1:92YTNmYXCSycERjKn/zPbeK5DiW3dd80j3+oVTEWTE8= +github.com/metacubex/quic-go v0.35.2-0.20230603072621-ea2663348ebb/go.mod h1:6pg8+Tje9KOltnj1whuvB2i5KFUMPp1TAF3oPhc5axM= +github.com/metacubex/sing v0.0.0-20230530121223-b768faae5c6b h1:Bw4j3ktf5vivi5qm/ZQGtyRAgybRKSGJaMV1t3rtC+I= +github.com/metacubex/sing v0.0.0-20230530121223-b768faae5c6b/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w= +github.com/metacubex/sing-shadowsocks v0.2.2 h1:prciO78IwtR4Sp+/CnP+aZSzpBRfL7zKaYez1S4EOnI= +github.com/metacubex/sing-shadowsocks v0.2.2/go.mod h1:haolI+8Yc8MhNDqNuoRP4X5vaquXWNYeL1YxrQZ5kCU= +github.com/metacubex/sing-shadowsocks2 v0.1.0 h1:ZxPEToY1RaRtG6ljz2n13ASMVqyAM7Bh11TmWoExYu4= +github.com/metacubex/sing-shadowsocks2 v0.1.0/go.mod h1:6C4EkvqMz5h7jECKrQeIByoLDHxiepsgPajIrxqxj/s= +github.com/metacubex/sing-tun v0.1.5-0.20230618235243-65051e73b018 h1:M7vBGA4RL4BBLSYfi15u/9QdVSqPkhuL4KRCuRhxuQY= +github.com/metacubex/sing-tun v0.1.5-0.20230618235243-65051e73b018/go.mod h1:DSVNjWT0rkkg8zn2+wpDvxgXuXRmMiNFDnVmnUctbAc= +github.com/metacubex/sing-vmess v0.1.5 h1:wODu17P27aGw0GhSIb/rIZWNh3/F5ghF/1PDDt95CQY= +github.com/metacubex/sing-vmess v0.1.5/go.mod h1:s00xTd3c/zOMQHyPec0G/pbUklndleiH0QaHZRd4Ykg= +github.com/metacubex/sing-wireguard v0.0.0-20230611155257-1498ae315a28 h1:mXFpxfR/1nADh+GoT8maWEvc6LO6uatPsARD8WzUDMA= +github.com/metacubex/sing-wireguard v0.0.0-20230611155257-1498ae315a28/go.mod h1:KrDPq/dE793jGIJw9kcIvjA/proAfU0IeU7WlMXW7rs= +github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= +github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mroth/weightedrand/v2 v2.0.1 h1:zrEVDIaau/E4QLOKu02kpg8T8myweFlMGikIgbIdrRA= +github.com/mroth/weightedrand/v2 v2.0.1/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.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +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/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= +github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= +github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= +github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +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/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-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= +github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= +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-mux v0.1.0 h1:xihlDRNs1J+hYwmvW9/ZmaghjDx7O0Y5dty0pOLQGB4= +github.com/sagernet/sing-mux v0.1.0/go.mod h1:i3jKjV4pRTFTV/ly5V3oa2JMPy0SAZ5X8X4tDU9Hw94= +github.com/sagernet/sing-shadowtls v0.1.2 h1:wkPf4gF+cmaP0cIbArpyq+mc6GcwbMx60CssmmhEQ0s= +github.com/sagernet/sing-shadowtls v0.1.2/go.mod h1:rTxhbSY8jGWZOWjdeOe1vP3E+hkgen8aRA2p7YccM88= +github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= +github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0= +github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 h1:2ItpW1nMNkPzmBTxV0/eClCklHrFSQMnUGcpUmJxVeE= +github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9/go.mod h1:FUyTEc5ye5NjKnDTDMuiLF2M6T4BE6y6KZuax//UCEg= +github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= +github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= +github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77 h1:g6QtRWQ2dKX7EQP++1JLNtw4C2TNxd4/ov8YUpOPOSo= +github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77/go.mod h1:pJDdXzZIwJ+2vmnT0TKzmf8meeum+e2mTDSehw79eE0= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= +github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y= +github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY= +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.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +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/xtls/go v0.0.0-20230107031059-4610f88d00f3 h1:a3Y4WVjCxwoyO4E2xdNvq577tW8lkSBgyrA8E9+2NtM= +github.com/xtls/go v0.0.0-20230107031059-4610f88d00f3/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= +github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= +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.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/core/lib.go b/core/lib.go new file mode 100644 index 00000000..8044b64a --- /dev/null +++ b/core/lib.go @@ -0,0 +1,75 @@ +package main + +/* +#include "stdint.h" +*/ +import "C" +import ( + "fmt" + "log" + "os" + "path/filepath" + "unsafe" + + "github.com/Dreamacro/clash/config" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub" + bridge "hiddify.com/hiddify/bridge" +) + +var options []hub.Option + +//export initNativeDartBridge +func initNativeDartBridge(api unsafe.Pointer) { + bridge.InitDartApi(api) +} + +//export setOptions +func setOptions(port C.longlong, dir *C.char, configPath *C.char) { + go func() { + dir := C.GoString(dir) + info, err := os.Stat(dir) + if err != nil { + log.Printf("[core] dir %s: %+v\n", dir, err) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + if !info.IsDir() { + log.Printf("[core] %s is not a directory\n", dir) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: "not directory"}) + return + } + constant.SetHomeDir(dir) + + path := C.GoString(configPath) + if !filepath.IsAbs(path) { + path = filepath.Join(dir, path) + } + constant.SetConfig(path) + + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true}) + }() +} + +//export start +func start(port C.longlong) { + go func() { + if err := config.Init(constant.Path.HomeDir()); err != nil { + log.Printf("[core] start error: init error: %+v\n", err) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + err := hub.Parse(options...) + if err != nil { + log.Printf("[core] start error: %+v\n", err) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true}) + }() + +} + +func main() { + fmt.Println("hello from clash native lib!") +} diff --git a/core/logs.go b/core/logs.go new file mode 100644 index 00000000..d648366f --- /dev/null +++ b/core/logs.go @@ -0,0 +1,55 @@ +package main + +import "C" +import ( + "encoding/json" + "fmt" + + "github.com/Dreamacro/clash/log" + bridge "hiddify.com/hiddify/bridge" +) + +var logSubscriber <-chan log.Event + +type Log struct { + Type string `json:"type"` + Payload string `json:"payload"` +} + +//export startLog +func startLog(port C.longlong, levelStr *C.char) { + levelTxt := C.GoString(levelStr) + if levelTxt == "" { + levelTxt = "info" + } + level := log.LogLevelMapping[levelTxt] + if logSubscriber != nil { + log.UnSubscribe(logSubscriber) + logSubscriber = nil + } + logSubscriber = log.Subscribe() + go func() { + for elem := range logSubscriber { + if elem.LogLevel < level { + continue + } + data, err := json.Marshal(Log{ + Type: elem.Type(), + Payload: elem.Payload, + }) + if err != nil { + fmt.Println("Error:", err) + } + bridge.SendStringToPort(int64(port), string(data)) + } + }() + fmt.Println("[GO] subscribe logger on dart bridge port", int64(port)) +} + +//export stopLog +func stopLog() { + if logSubscriber != nil { + log.UnSubscribe(logSubscriber) + logSubscriber = nil + } +} diff --git a/core/proxies.go b/core/proxies.go new file mode 100644 index 00000000..5abaa0b5 --- /dev/null +++ b/core/proxies.go @@ -0,0 +1,94 @@ +package main + +import "C" +import ( + "context" + "encoding/json" + "log" + "time" + + "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/outboundgroup" + "github.com/Dreamacro/clash/common/utils" + "github.com/Dreamacro/clash/component/profile/cachefile" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/tunnel" + bridge "hiddify.com/hiddify/bridge" +) + +//export getProxies +func getProxies(port C.longlong) { + proxies := tunnel.Proxies() + data, err := json.Marshal(map[string]map[string]constant.Proxy{"proxies": proxies}) + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true, Data: string(data)}) +} + +//export updateProxy +func updateProxy(port C.longlong, selectorName *C.char, proxyName *C.char) { + go func() { + proxies := tunnel.Proxies() + proxy := proxies[C.GoString(selectorName)] + if proxy == nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: "proxy doesn't exist"}) + return + } + adapter_proxy := proxy.(*adapter.Proxy) + selector, ok := adapter_proxy.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: "not a selector"}) + return + } + if err := selector.Set(C.GoString(proxyName)); err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + cachefile.Cache().SetSelected(string(C.GoString(selectorName)), string(C.GoString(proxyName))) + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true}) + }() +} + +//export getProxyDelay +func getProxyDelay(port C.longlong, name *C.char, url *C.char, timeout C.long) { + go func() { + proxy := tunnel.Proxies()[C.GoString(name)] + if proxy == nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: "proxy doesn't exist"}) + return + } + + log.Printf("%s before ctx", proxy.Name()) + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(int64(timeout))) + defer cancel() + + log.Printf("%s before expected status", proxy.Name()) + expectedStatus, err := utils.NewIntRanges[uint16]("200") + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + delay, err := proxy.URLTest(ctx, C.GoString(url), expectedStatus, constant.ExtraHistory) + if ctx.Err() != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: ctx.Err().Error()}) + return + } + log.Printf("%s after ctx check", proxy.Name()) + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + + log.Printf("%s before marshal", proxy.Name()) + data, err := json.Marshal(map[string]uint16{"delay": delay}) + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true, Data: string(data)}) + }() +} diff --git a/core/traffic.go b/core/traffic.go new file mode 100644 index 00000000..02e939a7 --- /dev/null +++ b/core/traffic.go @@ -0,0 +1,32 @@ +package main + +import "C" +import ( + "encoding/json" + + "github.com/Dreamacro/clash/tunnel/statistic" + bridge "hiddify.com/hiddify/bridge" +) + +type Traffic struct { + Up int64 `json:"up"` + Down int64 `json:"down"` +} + +//export getTraffic +func getTraffic(port C.longlong) { + go func() { + t := statistic.DefaultManager + up, down := t.Now() + traffic, err := json.Marshal(Traffic{ + Up: up, + Down: down, + }) + + if err != nil { + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: false, Message: err.Error()}) + return + } + bridge.SendResponseToPort(int64(port), &bridge.DartResponse{Success: true, Data: string(traffic)}) + }() +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..9625e105 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..592ceee8 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..592ceee8 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..02439c9d --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,613 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e42adcb3 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 00000000..9f447e1b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 00000000..3107d37f Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..00cabce8 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..df97d503 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..ceac52d7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..61708b85 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..05049113 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 00000000..7f9e1cd3 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Hiddify + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + hiddify + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + UIStatusBarHidden + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +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/lib/bootstrap.dart b/lib/bootstrap.dart new file mode 100644 index 00000000..bed69f1d --- /dev/null +++ b/lib/bootstrap.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:hiddify/core/app/app.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/features/system_tray/system_tray.dart'; +import 'package:hiddify/services/deep_link_service.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:loggy/loggy.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stack_trace/stack_trace.dart' as stack_trace; + +final _loggy = Loggy('bootstrap'); +final _stopWatch = Stopwatch(); + +Future lazyBootstrap(WidgetsBinding widgetsBinding) async { + _stopWatch.start(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + // temporary solution: https://github.com/rrousselGit/riverpod/issues/1874 + FlutterError.demangleStackTrace = (StackTrace stack) { + if (stack is stack_trace.Trace) return stack.vmTrace; + if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; + return stack; + }; + + final sharedPreferences = await SharedPreferences.getInstance(); + final container = ProviderContainer( + overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)], + ); + + Loggy.initLoggy(logPrinter: const PrettyPrinter()); + + await initAppServices(container.read); + await initControllers(container.read); + + runApp( + ProviderScope( + parent: container, + child: const AppView(), + ), + ); + + FlutterNativeSplash.remove(); + _stopWatch.stop(); + _loggy.debug("bootstrapping took [${_stopWatch.elapsedMilliseconds}]ms"); +} + +Future initAppServices( + Result Function(ProviderListenable) read, +) async { + await read(filesEditorServiceProvider).init(); + await Future.wait( + [ + read(connectivityServiceProvider).init(), + read(clashServiceProvider).init(), + read(clashServiceProvider).start(), + read(notificationServiceProvider).init(), + if (PlatformUtils.isDesktop) read(windowManagerServiceProvider).init(), + ], + ); + _loggy.debug('initialized app services'); +} + +Future initControllers( + Result Function(ProviderListenable) read, +) async { + await Future.wait( + [ + read(activeProfileProvider.future), + read(deepLinkServiceProvider.future), + if (PlatformUtils.isDesktop) read(systemTrayControllerProvider.future), + ], + ); + _loggy.debug("initialized base controllers"); +} diff --git a/lib/core/app/app.dart b/lib/core/app/app.dart new file mode 100644 index 00000000..f8f58a29 --- /dev/null +++ b/lib/core/app/app.dart @@ -0,0 +1 @@ +export 'app_view.dart'; diff --git a/lib/core/app/app_view.dart b/lib/core/app/app_view.dart new file mode 100644 index 00000000..988bb008 --- /dev/null +++ b/lib/core/app/app_view.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/core/theme/theme.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class AppView extends HookConsumerWidget with PresLogger { + const AppView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + final locale = ref.watch(localeControllerProvider).locale; + final theme = ref.watch(themeControllerProvider); + + return MaterialApp.router( + routerConfig: router, + locale: locale, + supportedLocales: LocalePref.locales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + debugShowCheckedModeBanner: false, + themeMode: theme.themeMode, + theme: theme.light, + darkTheme: theme.dark, + title: 'Hiddify', + ).animate().fadeIn(); + } +} diff --git a/lib/core/core_providers.dart b/lib/core/core_providers.dart new file mode 100644 index 00000000..97c6cc75 --- /dev/null +++ b/lib/core/core_providers.dart @@ -0,0 +1,8 @@ +import 'package:hiddify/core/locale/locale.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'core_providers.g.dart'; + +@Riverpod(keepAlive: true) +TranslationsEn translations(TranslationsRef ref) => + ref.watch(localeControllerProvider).translations(); diff --git a/lib/core/locale/locale.dart b/lib/core/locale/locale.dart new file mode 100644 index 00000000..f7731cb3 --- /dev/null +++ b/lib/core/locale/locale.dart @@ -0,0 +1,2 @@ +export 'locale_controller.dart'; +export 'locale_pref.dart'; diff --git a/lib/core/locale/locale_controller.dart b/lib/core/locale/locale_controller.dart new file mode 100644 index 00000000..3263dcc8 --- /dev/null +++ b/lib/core/locale/locale_controller.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/core/locale/locale_pref.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'locale_controller.g.dart'; + +@Riverpod(keepAlive: true) +class LocaleController extends _$LocaleController with AppLogger { + @override + LocalePref build() { + return LocalePref.values[_prefs.getInt(_localeKey) ?? 0]; + } + + static const _localeKey = 'locale'; + SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); + + Future change(LocalePref locale) async { + loggy.debug('changing locale to [$locale]'); + await _prefs.setInt(_localeKey, locale.index); + state = locale; + } +} diff --git a/lib/core/locale/locale_pref.dart b/lib/core/locale/locale_pref.dart new file mode 100644 index 00000000..ae25e09e --- /dev/null +++ b/lib/core/locale/locale_pref.dart @@ -0,0 +1,32 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hiddify/gen/translations.g.dart'; + +export 'package:hiddify/gen/translations.g.dart'; + +enum LocalePref { + en; + + Locale get locale { + return Locale(name); + } + + static List get locales => + LocalePref.values.map((e) => e.locale).toList(); + + static LocalePref fromString(String e) { + return LocalePref.values.firstOrNullWhere((element) => element.name == e) ?? + LocalePref.en; + } + + static LocalePref deviceLocale() { + return LocalePref.fromString( + AppLocaleUtils.findDeviceLocale().languageCode, + ); + } + + TranslationsEn translations() { + final appLocale = AppLocaleUtils.parse(name); + return appLocale.build(); + } +} diff --git a/lib/core/prefs/prefs.dart b/lib/core/prefs/prefs.dart new file mode 100644 index 00000000..a0657759 --- /dev/null +++ b/lib/core/prefs/prefs.dart @@ -0,0 +1,2 @@ +export 'prefs_controller.dart'; +export 'prefs_state.dart'; diff --git a/lib/core/prefs/prefs_controller.dart b/lib/core/prefs/prefs_controller.dart new file mode 100644 index 00000000..48556971 --- /dev/null +++ b/lib/core/prefs/prefs_controller.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:hiddify/core/prefs/prefs_state.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'prefs_controller.g.dart'; + +@Riverpod(keepAlive: true) +class PrefsController extends _$PrefsController with AppLogger { + @override + PrefsState build() { + return PrefsState( + clash: _getClashPrefs(), + network: _getNetworkPrefs(), + ); + } + + SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); + + static const _overridesKey = "clash_overrides"; + static const _networkKey = "clash_overrides"; + + ClashConfig _getClashPrefs() { + final persisted = _prefs.getString(_overridesKey); + if (persisted == null) return ClashConfig.initial; + return ClashConfig.fromJson(jsonDecode(persisted) as Map); + } + + NetworkPrefs _getNetworkPrefs() { + final persisted = _prefs.getString(_networkKey); + if (persisted == null) return const NetworkPrefs(); + return NetworkPrefs.fromJson(jsonDecode(persisted) as Map); + } + + Future patchClashOverrides(ClashConfigPatch overrides) async { + final newPrefs = state.clash.patch(overrides); + await _prefs.setString(_overridesKey, jsonEncode(newPrefs.toJson())); + state = state.copyWith(clash: newPrefs); + } + + Future patchNetworkPrefs({ + bool? systemProxy, + bool? bypassPrivateNetworks, + }) async { + final newPrefs = state.network.copyWith( + systemProxy: systemProxy ?? state.network.systemProxy, + bypassPrivateNetworks: + bypassPrivateNetworks ?? state.network.bypassPrivateNetworks, + ); + await _prefs.setString(_networkKey, jsonEncode(newPrefs.toJson())); + state = state.copyWith(network: newPrefs); + } +} diff --git a/lib/core/prefs/prefs_state.dart b/lib/core/prefs/prefs_state.dart new file mode 100644 index 00000000..23c8d55a --- /dev/null +++ b/lib/core/prefs/prefs_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; + +part 'prefs_state.freezed.dart'; + +@freezed +class PrefsState with _$PrefsState { + const PrefsState._(); + + const factory PrefsState({ + @Default(ClashConfig()) ClashConfig clash, + @Default(NetworkPrefs()) NetworkPrefs network, + }) = _PrefsState; +} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart new file mode 100644 index 00000000..67068e91 --- /dev/null +++ b/lib/core/router/app_router.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/services/deep_link_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_router.g.dart'; + +// TODO: test and improve handling of deep link +@riverpod +GoRouter router(RouterRef ref) { + final deepLink = ref.listen( + deepLinkServiceProvider, + (_, next) async { + if (next case AsyncData(value: final link?)) { + await ref.state.push( + NewProfileRoute(url: link.url, name: link.name).location, + ); + } + }, + ); + final initialLink = deepLink.read(); + String initialLocation = HomeRoute.path; + if (initialLink case AsyncData(value: final link?)) { + initialLocation = NewProfileRoute(url: link.url, name: link.name).location; + } + + return GoRouter( + navigatorKey: rootNavigatorKey, + initialLocation: initialLocation, + debugLogDiagnostics: true, + routes: $routes, + ); +} + +int getCurrentIndex(BuildContext context) { + final String location = GoRouterState.of(context).location; + if (location == HomeRoute.path) return 0; + if (location.startsWith(ProxiesRoute.path)) return 1; + if (location.startsWith(LogsRoute.path)) return 2; + if (location.startsWith(SettingsRoute.path)) return 3; + return 0; +} + +void switchTab(int index, BuildContext context) { + switch (index) { + case 0: + const HomeRoute().go(context); + case 1: + const ProxiesRoute().go(context); + case 2: + const LogsRoute().go(context); + case 3: + const SettingsRoute().go(context); + } +} diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart new file mode 100644 index 00000000..f557c5a6 --- /dev/null +++ b/lib/core/router/router.dart @@ -0,0 +1,2 @@ +export 'app_router.dart'; +export 'routes/routes.dart'; diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart new file mode 100644 index 00000000..bdcad07b --- /dev/null +++ b/lib/core/router/routes/desktop_routes.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/routes/shared_routes.dart'; +import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/view/view.dart'; +import 'package:hiddify/features/wrapper/wrapper.dart'; + +part 'desktop_routes.g.dart'; + +@TypedShellRoute( + routes: [ + TypedGoRoute( + path: HomeRoute.path, + routes: [ + TypedGoRoute(path: ProfilesRoute.path), + TypedGoRoute(path: NewProfileRoute.path), + TypedGoRoute(path: ProfileDetailsRoute.path), + ], + ), + TypedGoRoute(path: ProxiesRoute.path), + TypedGoRoute(path: LogsRoute.path), + TypedGoRoute(path: SettingsRoute.path), + ], +) +class DesktopWrapperRoute extends ShellRouteData { + const DesktopWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return DesktopWrapper(navigator); + } +} + +class LogsRoute extends GoRouteData { + const LogsRoute(); + static const path = '/logs'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage(child: LogsPage()); + } +} + +class SettingsRoute extends GoRouteData { + const SettingsRoute(); + static const path = '/settings'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage(child: SettingsPage()); + } +} diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart new file mode 100644 index 00000000..d1a1d5b0 --- /dev/null +++ b/lib/core/router/routes/mobile_routes.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/routes/shared_routes.dart'; +import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/view/view.dart'; +import 'package:hiddify/features/wrapper/wrapper.dart'; + +part 'mobile_routes.g.dart'; + +@TypedShellRoute( + routes: [ + TypedGoRoute( + path: HomeRoute.path, + routes: [ + TypedGoRoute(path: ProfilesRoute.path), + TypedGoRoute(path: NewProfileRoute.path), + TypedGoRoute(path: ProfileDetailsRoute.path), + ], + ), + TypedGoRoute(path: ProxiesRoute.path), + ], +) +class MobileWrapperRoute extends ShellRouteData { + const MobileWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return MobileWrapper(navigator); + } +} + +@TypedGoRoute(path: LogsRoute.path) +class LogsRoute extends GoRouteData { + const LogsRoute(); + static const path = '/logs'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + child: LogsPage(), + ); + } +} + +@TypedGoRoute(path: SettingsRoute.path) +class SettingsRoute extends GoRouteData { + const SettingsRoute(); + static const path = '/settings'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + child: SettingsPage(), + ); + } +} diff --git a/lib/core/router/routes/routes.dart b/lib/core/router/routes/routes.dart new file mode 100644 index 00000000..1cc0940d --- /dev/null +++ b/lib/core/router/routes/routes.dart @@ -0,0 +1,16 @@ +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop; +import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile; +import 'package:hiddify/core/router/routes/shared_routes.dart' as shared; +import 'package:hiddify/utils/utils.dart'; + +export 'mobile_routes.dart'; +export 'shared_routes.dart' hide $appRoutes; + +List get $routes => [ + if (PlatformUtils.isDesktop) + ...desktop.$appRoutes + else + ...mobile.$appRoutes, + ...shared.$appRoutes, + ]; diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart new file mode 100644 index 00000000..0ecd1a4b --- /dev/null +++ b/lib/core/router/routes/shared_routes.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/home/view/view.dart'; +import 'package:hiddify/features/profile_detail/view/view.dart'; +import 'package:hiddify/features/profiles/view/view.dart'; +import 'package:hiddify/features/proxies/view/view.dart'; +import 'package:hiddify/utils/utils.dart'; + +part 'shared_routes.g.dart'; + +List get $sharedRoutes => $appRoutes; + +final GlobalKey rootNavigatorKey = GlobalKey(); + +class HomeRoute extends GoRouteData { + const HomeRoute(); + static const path = '/'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage(child: HomePage()); + } +} + +class ProxiesRoute extends GoRouteData { + const ProxiesRoute(); + static const path = '/proxies'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage(child: ProxiesPage()); + } +} + +@TypedGoRoute(path: AddProfileRoute.path) +class AddProfileRoute extends GoRouteData { + const AddProfileRoute(); + static const path = '/add'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + fixed: true, + builder: (controller) => AddProfileModal(scrollController: controller), + ); + } +} + +class ProfilesRoute extends GoRouteData { + const ProfilesRoute(); + static const path = 'profiles'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + builder: (controller) => ProfilesModal(scrollController: controller), + ); + } +} + +class NewProfileRoute extends GoRouteData { + const NewProfileRoute({this.url, this.name}); + static const path = 'profiles/new'; + final String? url; + final String? name; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return MaterialPage( + fullscreenDialog: true, + child: ProfileDetailPage( + "new", + url: url, + name: name, + ), + ); + } +} + +class ProfileDetailsRoute extends GoRouteData { + const ProfileDetailsRoute(this.id); + final String id; + static const path = 'profiles/:id'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return MaterialPage( + fullscreenDialog: true, + child: ProfileDetailPage(id), + ); + } +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 00000000..2f3f9600 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,123 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/theme/theme_prefs.dart'; + +// mostly exact copy of flex color scheme 7.1's fabulous 12 theme +extension AppTheme on ThemePrefs { + ThemeData get light { + return FlexThemeData.light( + scheme: FlexScheme.indigoM3, + surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, + useMaterial3: true, + swapLegacyOnMaterial3: true, + useMaterial3ErrorColors: true, + blendLevel: 1, + subThemesData: const FlexSubThemesData( + useTextTheme: true, + useM2StyleDividerInM3: true, + elevatedButtonSchemeColor: SchemeColor.onPrimaryContainer, + elevatedButtonSecondarySchemeColor: SchemeColor.primaryContainer, + outlinedButtonOutlineSchemeColor: SchemeColor.primary, + toggleButtonsBorderSchemeColor: SchemeColor.primary, + segmentedButtonSchemeColor: SchemeColor.primary, + segmentedButtonBorderSchemeColor: SchemeColor.primary, + unselectedToggleIsColored: true, + sliderValueTinted: true, + inputDecoratorSchemeColor: SchemeColor.primary, + inputDecoratorBackgroundAlpha: 43, + inputDecoratorUnfocusedHasBorder: false, + inputDecoratorFocusedBorderWidth: 1.0, + inputDecoratorPrefixIconSchemeColor: SchemeColor.primary, + popupMenuRadius: 8.0, + popupMenuElevation: 3.0, + drawerIndicatorSchemeColor: SchemeColor.primary, + bottomNavigationBarMutedUnselectedLabel: false, + bottomNavigationBarMutedUnselectedIcon: false, + menuRadius: 8.0, + menuElevation: 3.0, + menuBarRadius: 0.0, + menuBarElevation: 2.0, + menuBarShadowColor: Color(0x00000000), + navigationBarSelectedLabelSchemeColor: SchemeColor.primary, + navigationBarMutedUnselectedLabel: false, + navigationBarSelectedIconSchemeColor: SchemeColor.onPrimary, + navigationBarMutedUnselectedIcon: false, + navigationBarIndicatorSchemeColor: SchemeColor.primary, + navigationBarIndicatorOpacity: 1.00, + navigationRailSelectedLabelSchemeColor: SchemeColor.primary, + navigationRailMutedUnselectedLabel: false, + navigationRailSelectedIconSchemeColor: SchemeColor.onPrimary, + navigationRailMutedUnselectedIcon: false, + navigationRailIndicatorSchemeColor: SchemeColor.primary, + navigationRailIndicatorOpacity: 1.00, + navigationRailBackgroundSchemeColor: SchemeColor.surface, + ), + keyColors: const FlexKeyColors( + useSecondary: true, + useTertiary: true, + keepPrimary: true, + ), + tones: FlexTones.jolly(Brightness.light), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + ); + } + + ThemeData get dark { + return FlexThemeData.dark( + scheme: FlexScheme.indigoM3, + useMaterial3: true, + swapLegacyOnMaterial3: true, + useMaterial3ErrorColors: true, + darkIsTrueBlack: trueBlack, + surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, + // blendLevel: 1, + subThemesData: const FlexSubThemesData( + blendTextTheme: true, + useTextTheme: true, + useM2StyleDividerInM3: true, + elevatedButtonSchemeColor: SchemeColor.onPrimaryContainer, + elevatedButtonSecondarySchemeColor: SchemeColor.primaryContainer, + outlinedButtonOutlineSchemeColor: SchemeColor.primary, + toggleButtonsBorderSchemeColor: SchemeColor.primary, + segmentedButtonSchemeColor: SchemeColor.primary, + segmentedButtonBorderSchemeColor: SchemeColor.primary, + unselectedToggleIsColored: true, + sliderValueTinted: true, + inputDecoratorSchemeColor: SchemeColor.primary, + inputDecoratorBackgroundAlpha: 43, + inputDecoratorUnfocusedHasBorder: false, + inputDecoratorFocusedBorderWidth: 1.0, + inputDecoratorPrefixIconSchemeColor: SchemeColor.primary, + popupMenuRadius: 8.0, + popupMenuElevation: 3.0, + drawerIndicatorSchemeColor: SchemeColor.primary, + bottomNavigationBarMutedUnselectedLabel: false, + bottomNavigationBarMutedUnselectedIcon: false, + menuRadius: 8.0, + menuElevation: 3.0, + menuBarRadius: 0.0, + menuBarElevation: 2.0, + menuBarShadowColor: Color(0x00000000), + navigationBarSelectedLabelSchemeColor: SchemeColor.primary, + navigationBarMutedUnselectedLabel: false, + navigationBarSelectedIconSchemeColor: SchemeColor.onPrimary, + navigationBarMutedUnselectedIcon: false, + navigationBarIndicatorSchemeColor: SchemeColor.primary, + navigationBarIndicatorOpacity: 1.00, + navigationRailSelectedLabelSchemeColor: SchemeColor.primary, + navigationRailMutedUnselectedLabel: false, + navigationRailSelectedIconSchemeColor: SchemeColor.onPrimary, + navigationRailMutedUnselectedIcon: false, + navigationRailIndicatorSchemeColor: SchemeColor.primary, + navigationRailIndicatorOpacity: 1.00, + navigationRailBackgroundSchemeColor: SchemeColor.surface, + ), + keyColors: const FlexKeyColors( + useSecondary: true, + useTertiary: true, + ), + // tones: FlexTones.jolly(Brightness.dark), + visualDensity: FlexColorScheme.comfortablePlatformDensity, + ); + } +} diff --git a/lib/core/theme/constants.dart b/lib/core/theme/constants.dart new file mode 100644 index 00000000..ed48e95b --- /dev/null +++ b/lib/core/theme/constants.dart @@ -0,0 +1,6 @@ +import 'package:flutter/widgets.dart'; + +abstract class ConnectionButtonColor { + static const connected = Color.fromRGBO(89, 140, 82, 1); + static const disconnected = Color.fromRGBO(74, 77, 139, 1); +} diff --git a/lib/core/theme/theme.dart b/lib/core/theme/theme.dart new file mode 100644 index 00000000..d03eb006 --- /dev/null +++ b/lib/core/theme/theme.dart @@ -0,0 +1,4 @@ +export 'app_theme.dart'; +export 'constants.dart'; +export 'theme_controller.dart'; +export 'theme_prefs.dart'; diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart new file mode 100644 index 00000000..2549661d --- /dev/null +++ b/lib/core/theme/theme_controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/theme/theme_prefs.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'theme_controller.g.dart'; + +@Riverpod(keepAlive: true) +class ThemeController extends _$ThemeController with AppLogger { + @override + ThemePrefs build() { + return ThemePrefs( + themeMode: ThemeMode.values[_prefs.getInt(_themeModeKey) ?? 0], + trueBlack: _prefs.getBool(_trueBlackKey) ?? false, + ); + } + + SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); + + static const _themeModeKey = "theme_mode"; + static const _trueBlackKey = "true_black"; + + Future change({ + ThemeMode? themeMode, + bool? trueBlack, + }) async { + loggy.debug('changing theme, mode=$themeMode, trueBlack=$trueBlack'); + if (themeMode != null) { + await _prefs.setInt(_themeModeKey, themeMode.index); + } + if (trueBlack != null) { + await _prefs.setBool(_trueBlackKey, trueBlack); + } + state = state.copyWith( + themeMode: themeMode ?? state.themeMode, + trueBlack: trueBlack ?? state.trueBlack, + ); + } +} diff --git a/lib/core/theme/theme_prefs.dart b/lib/core/theme/theme_prefs.dart new file mode 100644 index 00000000..9701d66e --- /dev/null +++ b/lib/core/theme/theme_prefs.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'theme_prefs.freezed.dart'; + +@freezed +class ThemePrefs with _$ThemePrefs { + const ThemePrefs._(); + + const factory ThemePrefs({ + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(false) bool trueBlack, + }) = _ThemePrefs; +} diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart new file mode 100644 index 00000000..f49e403f --- /dev/null +++ b/lib/data/data_providers.dart @@ -0,0 +1,42 @@ +import 'package:dio/dio.dart'; +import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/repository/repository.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'data_providers.g.dart'; + +@Riverpod(keepAlive: true) +AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect(); + +@Riverpod(keepAlive: true) +SharedPreferences sharedPreferences(SharedPreferencesRef ref) => + throw UnimplementedError('sharedPreferences must be overridden'); + +// TODO: set options for dio +@Riverpod(keepAlive: true) +Dio dio(DioRef ref) => Dio(); + +@Riverpod(keepAlive: true) +ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao( + ref.watch(appDatabaseProvider), + ); + +@Riverpod(keepAlive: true) +ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl( + clashService: ref.watch(clashServiceProvider), + filesEditor: ref.watch(filesEditorServiceProvider), + ); + +@Riverpod(keepAlive: true) +ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) => + ProfilesRepositoryImpl( + profilesDao: ref.watch(profilesDaoProvider), + filesEditor: ref.watch(filesEditorServiceProvider), + clashFacade: ref.watch(clashFacadeProvider), + dio: ref.watch(dioProvider), + ); diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart new file mode 100644 index 00000000..1d0c3ad7 --- /dev/null +++ b/lib/data/local/dao/dao.dart @@ -0,0 +1 @@ +export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/profiles_dao.dart b/lib/data/local/dao/profiles_dao.dart new file mode 100644 index 00000000..f9198a97 --- /dev/null +++ b/lib/data/local/dao/profiles_dao.dart @@ -0,0 +1,83 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/data_mappers.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/utils/utils.dart'; + +part 'profiles_dao.g.dart'; + +@DriftAccessor(tables: [ProfileEntries]) +class ProfilesDao extends DatabaseAccessor + with _$ProfilesDaoMixin, InfraLogger { + ProfilesDao(super.db); + + Future getById(String id) async { + return (profileEntries.select()..where((tbl) => tbl.id.equals(id))) + .map(ProfileMapper.fromEntry) + .getSingleOrNull(); + } + + Stream watchActiveProfile() { + return (profileEntries.select()..where((tbl) => tbl.active.equals(true))) + .map(ProfileMapper.fromEntry) + .watchSingleOrNull(); + } + + Stream watchProfileCount() { + final count = profileEntries.id.count(); + return (profileEntries.selectOnly()..addColumns([count])) + .map((exp) => exp.read(count)!) + .watchSingle(); + } + + Stream> watchAll() { + return (profileEntries.select() + ..orderBy( + [(tbl) => OrderingTerm.desc(tbl.active)], + )) + .map(ProfileMapper.fromEntry) + .watch(); + } + + Future create(Profile profile) async { + await transaction( + () async { + if (profile.active) { + await (update(profileEntries) + ..where((tbl) => tbl.id.isNotValue(profile.id))) + .write(const ProfileEntriesCompanion(active: Value(false))); + } + await into(profileEntries).insert(profile.toCompanion()); + }, + ); + } + + Future edit(Profile patch) async { + await transaction( + () async { + await (update(profileEntries)..where((tbl) => tbl.id.equals(patch.id))) + .write(patch.toCompanion()); + }, + ); + } + + Future setAsActive(String id) async { + await transaction( + () async { + await (update(profileEntries)..where((tbl) => tbl.id.isNotValue(id))) + .write(const ProfileEntriesCompanion(active: Value(false))); + await (update(profileEntries)..where((tbl) => tbl.id.equals(id))) + .write(const ProfileEntriesCompanion(active: Value(true))); + }, + ); + } + + Future removeById(String id) async { + await transaction( + () async { + await (delete(profileEntries)..where((tbl) => tbl.id.equals(id))).go(); + }, + ); + } +} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart new file mode 100644 index 00000000..800fcfc3 --- /dev/null +++ b/lib/data/local/data_mappers.dart @@ -0,0 +1,37 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; + +extension ProfileMapper on Profile { + ProfileEntriesCompanion toCompanion() { + return ProfileEntriesCompanion.insert( + id: id, + active: active, + name: name, + url: url, + lastUpdate: lastUpdate, + upload: Value(subInfo?.upload), + download: Value(subInfo?.download), + total: Value(subInfo?.total), + expire: Value(subInfo?.expire), + updateInterval: Value(updateInterval), + ); + } + + static Profile fromEntry(ProfileEntry entry) { + return Profile( + id: entry.id, + active: entry.active, + name: entry.name, + url: entry.url, + lastUpdate: entry.lastUpdate, + updateInterval: entry.updateInterval, + subInfo: SubscriptionInfo( + upload: entry.upload, + download: entry.download, + total: entry.total, + expire: entry.expire, + ), + ); + } +} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart new file mode 100644 index 00000000..9eb11172 --- /dev/null +++ b/lib/data/local/database.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/data/local/type_converters.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +part 'database.g.dart'; + +@DriftDatabase(tables: [ProfileEntries], daos: [ProfilesDao]) +class AppDatabase extends _$AppDatabase { + AppDatabase({required QueryExecutor connection}) : super(connection); + + AppDatabase.connect() : super(_openConnection()); + + @override + int get schemaVersion => 1; +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'db.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart new file mode 100644 index 00000000..0f4726a1 --- /dev/null +++ b/lib/data/local/tables.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/type_converters.dart'; + +@DataClassName('ProfileEntry') +class ProfileEntries extends Table { + TextColumn get id => text()(); + BoolColumn get active => boolean()(); + TextColumn get name => text().withLength(min: 1)(); + TextColumn get url => text()(); + IntColumn get upload => integer().nullable()(); + IntColumn get download => integer().nullable()(); + IntColumn get total => integer().nullable()(); + DateTimeColumn get expire => dateTime().nullable()(); + IntColumn get updateInterval => + integer().nullable().map(DurationTypeConverter())(); + DateTimeColumn get lastUpdate => dateTime()(); + + @override + Set get primaryKey => {id}; +} diff --git a/lib/data/local/type_converters.dart b/lib/data/local/type_converters.dart new file mode 100644 index 00000000..a66a511e --- /dev/null +++ b/lib/data/local/type_converters.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; + +class DurationTypeConverter extends TypeConverter { + @override + Duration fromSql(int fromDb) { + return Duration(seconds: fromDb); + } + + @override + int toSql(Duration value) { + return value.inSeconds; + } +} diff --git a/lib/data/repository/clash_facade_impl.dart b/lib/data/repository/clash_facade_impl.dart new file mode 100644 index 00000000..3d583879 --- /dev/null +++ b/lib/data/repository/clash_facade_impl.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/services/clash/clash.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/utils/utils.dart'; + +class ClashFacadeImpl + with ExceptionHandler, InfraLogger + implements ClashFacade { + ClashFacadeImpl({ + required ClashService clashService, + required FilesEditorService filesEditor, + }) : _clash = clashService, + _filesEditor = filesEditor; + + final ClashService _clash; + final FilesEditorService _filesEditor; + + @override + TaskEither getConfigs() { + return exceptionHandler( + () async => _clash.getConfigs().mapLeft(ClashFailure.core).run(), + ClashFailure.unexpected, + ); + } + + @override + TaskEither validateConfig(String configFileName) { + return exceptionHandler( + () async { + final path = _filesEditor.configPath(configFileName); + return _clash.validateConfig(path).mapLeft(ClashFailure.core).run(); + }, + ClashFailure.unexpected, + ); + } + + @override + TaskEither changeConfigs(String configFileName) { + return exceptionHandler( + () async { + loggy.debug("changing config, file name: [$configFileName]"); + final path = _filesEditor.configPath(configFileName); + return _clash.updateConfigs(path).mapLeft(ClashFailure.core).run(); + }, + ClashFailure.unexpected, + ); + } + + @override + TaskEither patchOverrides(ClashConfig overrides) { + return exceptionHandler( + () async => + _clash.patchConfigs(overrides).mapLeft(ClashFailure.core).run(), + ClashFailure.unexpected, + ); + } + + @override + TaskEither> getProxies() { + return exceptionHandler( + () async => _clash.getProxies().mapLeft(ClashFailure.core).run(), + ClashFailure.unexpected, + ); + } + + @override + TaskEither changeProxy( + String selectorName, + String proxyName, + ) { + return exceptionHandler( + () async => _clash + .changeProxy(selectorName, proxyName) + .mapLeft(ClashFailure.core) + .run(), + ClashFailure.unexpected, + ); + } + + @override + TaskEither getTraffic() { + return exceptionHandler( + () async => _clash.getTraffic().mapLeft(ClashFailure.core).run(), + ClashFailure.unexpected, + ); + } + + @override + TaskEither testDelay( + String proxyName, { + String testUrl = Constants.delayTestUrl, + }) { + return exceptionHandler( + () async { + final result = _clash + .getProxyDelay(proxyName, testUrl) + .mapLeft(ClashFailure.core) + .run(); + return result; + }, + ClashFailure.unexpected, + ); + } + + @override + Stream> watchLogs() { + return _clash + .watchLogs(LogLevel.info) + .handleExceptions(ClashFailure.unexpected); + } +} diff --git a/lib/data/repository/exception_handlers.dart b/lib/data/repository/exception_handlers.dart new file mode 100644 index 00000000..3805b584 --- /dev/null +++ b/lib/data/repository/exception_handlers.dart @@ -0,0 +1,32 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:rxdart/rxdart.dart'; + +mixin ExceptionHandler implements LoggerMixin { + TaskEither exceptionHandler( + Future> Function() run, + F Function(Object error, StackTrace stackTrace) onError, + ) { + return TaskEither( + () async { + try { + return await run(); + } catch (error, stackTrace) { + return Left(onError(error, stackTrace)); + } + }, + ); + } +} + +extension StreamExceptionHandler on Stream { + Stream> handleExceptions( + F Function(Object error, StackTrace stackTrace) onError, + ) { + return map(right).onErrorReturnWith( + (error, stackTrace) { + return Left(onError(error, stackTrace)); + }, + ); + } +} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/data/repository/profiles_repository_impl.dart new file mode 100644 index 00000000..179b9264 --- /dev/null +++ b/lib/data/repository/profiles_repository_impl.dart @@ -0,0 +1,155 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:meta/meta.dart'; + +class ProfilesRepositoryImpl + with ExceptionHandler, InfraLogger + implements ProfilesRepository { + ProfilesRepositoryImpl({ + required this.profilesDao, + required this.filesEditor, + required this.clashFacade, + required this.dio, + }); + + final ProfilesDao profilesDao; + final FilesEditorService filesEditor; + final ClashFacade clashFacade; + final Dio dio; + + @override + TaskEither get(String id) { + return TaskEither.tryCatch( + () => profilesDao.getById(id), + ProfileUnexpectedFailure.new, + ); + } + + @override + Stream> watchActiveProfile() { + return profilesDao + .watchActiveProfile() + .handleExceptions(ProfileUnexpectedFailure.new); + } + + @override + Stream> watchHasAnyProfile() { + return profilesDao + .watchProfileCount() + .map((event) => event != 0) + .handleExceptions(ProfileUnexpectedFailure.new); + } + + @override + Stream>> watchAll() { + return profilesDao + .watchAll() + .handleExceptions(ProfileUnexpectedFailure.new); + } + + @override + TaskEither add(Profile baseProfile) { + return exceptionHandler( + () async { + return fetch(baseProfile.url, baseProfile.id) + .flatMap( + (subInfo) => TaskEither(() async { + await profilesDao.create( + baseProfile.copyWith( + subInfo: subInfo, + lastUpdate: DateTime.now(), + ), + ); + return right(unit); + }), + ) + .run(); + }, + ProfileUnexpectedFailure.new, + ); + } + + @override + TaskEither update(Profile baseProfile) { + return exceptionHandler( + () async { + return fetch(baseProfile.url, baseProfile.id) + .flatMap( + (subInfo) => TaskEither(() async { + await profilesDao.edit( + baseProfile.copyWith( + subInfo: subInfo, + lastUpdate: DateTime.now(), + ), + ); + return right(unit); + }), + ) + .run(); + }, + ProfileUnexpectedFailure.new, + ); + } + + @override + TaskEither setAsActive(String id) { + return TaskEither.tryCatch( + () async { + await profilesDao.setAsActive(id); + return unit; + }, + ProfileUnexpectedFailure.new, + ); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + await profilesDao.removeById(id); + await filesEditor.deleteConfig(id); + return unit; + }, + ProfileUnexpectedFailure.new, + ); + } + + @visibleForTesting + TaskEither fetch( + String url, + String fileName, + ) { + return TaskEither( + () async { + final path = filesEditor.configPath(fileName); + final response = await dio.download(url, path); + if (response.statusCode != 200) { + await File(path).delete(); + return left(const ProfileUnexpectedFailure()); + } + final isValid = await clashFacade + .validateConfig(fileName) + .getOrElse((_) => false) + .run(); + if (!isValid) { + await File(path).delete(); + return left(const ProfileFailure.invalidConfig()); + } + final subInfoString = + response.headers.map['subscription-userinfo']?.single; + final subInfo = subInfoString != null + ? SubscriptionInfo.fromResponseHeader(subInfoString) + : null; + return right(subInfo); + }, + ); + } +} diff --git a/lib/data/repository/repository.dart b/lib/data/repository/repository.dart new file mode 100644 index 00000000..1d44b8b7 --- /dev/null +++ b/lib/data/repository/repository.dart @@ -0,0 +1,2 @@ +export 'clash_facade_impl.dart'; +export 'profiles_repository_impl.dart'; diff --git a/lib/domain/clash/clash.dart b/lib/domain/clash/clash.dart new file mode 100644 index 00000000..a7a9c962 --- /dev/null +++ b/lib/domain/clash/clash.dart @@ -0,0 +1,7 @@ +export 'clash_config.dart'; +export 'clash_enums.dart'; +export 'clash_facade.dart'; +export 'clash_failures.dart'; +export 'clash_log.dart'; +export 'clash_proxy.dart'; +export 'clash_traffic.dart'; diff --git a/lib/domain/clash/clash_config.dart b/lib/domain/clash/clash_config.dart new file mode 100644 index 00000000..2779ddd7 --- /dev/null +++ b/lib/domain/clash/clash_config.dart @@ -0,0 +1,72 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash_enums.dart'; + +part 'clash_config.freezed.dart'; +part 'clash_config.g.dart'; + +@freezed +class ClashConfig with _$ClashConfig { + const ClashConfig._(); + + @JsonSerializable(includeIfNull: false, fieldRename: FieldRename.kebab) + const factory ClashConfig({ + @JsonKey(name: 'port') int? httpPort, + int? socksPort, + int? redirPort, + int? tproxyPort, + int? mixedPort, + List? authentication, + bool? allowLan, + String? bindAddress, + TunnelMode? mode, + LogLevel? logLevel, + bool? ipv6, + }) = _ClashConfig; + + static const initial = ClashConfig( + httpPort: 12346, + socksPort: 12347, + mixedPort: 12348, + ); + + ClashConfig patch(ClashConfigPatch patch) { + return copyWith( + httpPort: (patch.httpPort ?? optionOf(httpPort)).toNullable(), + socksPort: (patch.socksPort ?? optionOf(socksPort)).toNullable(), + redirPort: (patch.redirPort ?? optionOf(redirPort)).toNullable(), + tproxyPort: (patch.tproxyPort ?? optionOf(tproxyPort)).toNullable(), + mixedPort: (patch.mixedPort ?? optionOf(mixedPort)).toNullable(), + authentication: + (patch.authentication ?? optionOf(authentication)).toNullable(), + allowLan: (patch.allowLan ?? optionOf(allowLan)).toNullable(), + bindAddress: (patch.bindAddress ?? optionOf(bindAddress)).toNullable(), + mode: (patch.mode ?? optionOf(mode)).toNullable(), + logLevel: (patch.logLevel ?? optionOf(logLevel)).toNullable(), + ipv6: (patch.ipv6 ?? optionOf(ipv6)).toNullable(), + ); + } + + factory ClashConfig.fromJson(Map json) => + _$ClashConfigFromJson(json); +} + +@freezed +class ClashConfigPatch with _$ClashConfigPatch { + const ClashConfigPatch._(); + + @JsonSerializable(includeIfNull: false) + const factory ClashConfigPatch({ + Option? httpPort, + Option? socksPort, + Option? redirPort, + Option? tproxyPort, + Option? mixedPort, + Option>? authentication, + Option? allowLan, + Option? bindAddress, + Option? mode, + Option? logLevel, + Option? ipv6, + }) = _ClashConfigPatch; +} diff --git a/lib/domain/clash/clash_enums.dart b/lib/domain/clash/clash_enums.dart new file mode 100644 index 00000000..1516c874 --- /dev/null +++ b/lib/domain/clash/clash_enums.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +enum TunnelMode { + rule, + global, + direct; +} + +enum LogLevel { + info, + warning, + error, + debug, + silent; + + Color get color => switch (this) { + info => Colors.lightGreen, + warning => Colors.orangeAccent, + error => Colors.redAccent, + debug => Colors.lightBlue, + _ => Colors.white, + }; +} + +enum ProxyType { + direct("Direct"), + reject("Reject"), + compatible("Compatible"), + pass("Pass"), + shadowSocks("ShadowSocks"), + shadowSocksR("ShadowSocksR"), + snell("Snell"), + socks5("Socks5"), + http("Http"), + vmess("Vmess"), + vless("Vless"), + trojan("Trojan"), + hysteria("Hysteria"), + wireGuard("WireGuard"), + tuic("Tuic"), + relay("Relay"), + selector("Selector"), + fallback("Fallback"), + urlTest("URLTest", "urltest"), + loadBalance("LoadBalance"), + unknown("Unknown"); + + const ProxyType(this.label, [this._key]); + + final String? _key; + final String label; + + String get key => _key ?? name; + + static List groupValues = [ + selector, + fallback, + urlTest, + loadBalance, + ]; +} diff --git a/lib/domain/clash/clash_facade.dart b/lib/domain/clash/clash_facade.dart new file mode 100644 index 00000000..210a0662 --- /dev/null +++ b/lib/domain/clash/clash_facade.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/constants.dart'; + +abstract class ClashFacade { + TaskEither getConfigs(); + + TaskEither validateConfig(String configFileName); + + /// change active configuration file by [configFileName] + TaskEither changeConfigs(String configFileName); + + TaskEither patchOverrides(ClashConfig overrides); + + TaskEither> getProxies(); + + TaskEither changeProxy( + String selectorName, + String proxyName, + ); + + TaskEither testDelay( + String proxyName, { + String testUrl = Constants.delayTestUrl, + }); + + TaskEither getTraffic(); + + Stream> watchLogs(); +} diff --git a/lib/domain/clash/clash_failures.dart b/lib/domain/clash/clash_failures.dart new file mode 100644 index 00000000..249e366e --- /dev/null +++ b/lib/domain/clash/clash_failures.dart @@ -0,0 +1,27 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'clash_failures.freezed.dart'; + +// TODO: rewrite +@freezed +sealed class ClashFailure with _$ClashFailure, Failure { + const ClashFailure._(); + + const factory ClashFailure.unexpected( + Object error, + StackTrace stackTrace, + ) = ClashUnexpectedFailure; + + const factory ClashFailure.core([String? error]) = ClashCoreFailure; + + @override + String present(TranslationsEn t) { + return switch (this) { + ClashUnexpectedFailure() => t.failure.clash.unexpected, + ClashCoreFailure(:final error) => + t.failure.clash.core(reason: error ?? ""), + }; + } +} diff --git a/lib/domain/clash/clash_log.dart b/lib/domain/clash/clash_log.dart new file mode 100644 index 00000000..c016e6ba --- /dev/null +++ b/lib/domain/clash/clash_log.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash_enums.dart'; + +part 'clash_log.freezed.dart'; +part 'clash_log.g.dart'; + +@freezed +class ClashLog with _$ClashLog { + const ClashLog._(); + + const factory ClashLog({ + @JsonKey(name: 'type') required LogLevel level, + @JsonKey(name: 'payload') required String message, + @JsonKey(defaultValue: DateTime.now) required DateTime time, + }) = _ClashLog; + + String get timeStamp => + "${time.month}-${time.day} ${time.hour}:${time.minute}:${time.second}"; + + factory ClashLog.fromJson(Map json) => + _$ClashLogFromJson(json); +} diff --git a/lib/domain/clash/clash_proxy.dart b/lib/domain/clash/clash_proxy.dart new file mode 100644 index 00000000..81b020ce --- /dev/null +++ b/lib/domain/clash/clash_proxy.dart @@ -0,0 +1,59 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash_enums.dart'; + +part 'clash_proxy.freezed.dart'; +part 'clash_proxy.g.dart'; + +// TODO: test and improve +@Freezed(fromJson: true) +class ClashProxy with _$ClashProxy { + const ClashProxy._(); + + const factory ClashProxy.group({ + required String name, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + required List all, + required String now, + List? history, + @JsonKey(includeFromJson: false, includeToJson: false) int? delay, + }) = ClashProxyGroup; + + const factory ClashProxy.item({ + required String name, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + List? history, + @JsonKey(includeFromJson: false, includeToJson: false) int? delay, + }) = ClashProxyItem; + + factory ClashProxy.fromJson(Map json) { + final isGroup = json.containsKey('all') || + json.containsKey('now') || + ProxyType.groupValues.any( + (e) => e.label == json.getOrElse('type', () => null), + ); + if (isGroup) { + return ClashProxyGroup.fromJson(json); + } else { + return ClashProxyItem.fromJson(json); + } + } +} + +ProxyType _typeFromJson(dynamic type) => + ProxyType.values + .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? + ProxyType.unknown; + +@freezed +class ClashHistory with _$ClashHistory { + const ClashHistory._(); + + const factory ClashHistory({ + required String time, + required int delay, + }) = _ClashHistory; + + factory ClashHistory.fromJson(Map json) => + _$ClashHistoryFromJson(json); +} diff --git a/lib/domain/clash/clash_traffic.dart b/lib/domain/clash/clash_traffic.dart new file mode 100644 index 00000000..8602bc26 --- /dev/null +++ b/lib/domain/clash/clash_traffic.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'clash_traffic.freezed.dart'; +part 'clash_traffic.g.dart'; + +@freezed +class ClashTraffic with _$ClashTraffic { + const ClashTraffic._(); + + const factory ClashTraffic({ + @JsonKey(name: 'up') required int upload, + @JsonKey(name: 'down') required int download, + }) = _ClashTraffic; + + factory ClashTraffic.fromJson(Map json) => + _$ClashTrafficFromJson(json); +} diff --git a/lib/domain/connectivity/connection_status.dart b/lib/domain/connectivity/connection_status.dart new file mode 100644 index 00000000..048951b2 --- /dev/null +++ b/lib/domain/connectivity/connection_status.dart @@ -0,0 +1,43 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/domain/connectivity/connectivity_failure.dart'; + +part 'connection_status.freezed.dart'; + +@freezed +sealed class ConnectionStatus with _$ConnectionStatus { + const ConnectionStatus._(); + + const factory ConnectionStatus.disconnected([ + ConnectivityFailure? connectFailure, + ]) = Disconnected; + const factory ConnectionStatus.connecting() = Connecting; + const factory ConnectionStatus.connected([ + ConnectivityFailure? disconnectFailure, + ]) = Connected; + const factory ConnectionStatus.disconnecting() = Disconnecting; + + factory ConnectionStatus.fromBool(bool connected) { + return connected + ? const ConnectionStatus.connected() + : const Disconnected(); + } + + bool get isConnected => switch (this) { + Connected() => true, + _ => false, + }; + + bool get isSwitching => switch (this) { + Connecting() => true, + Disconnecting() => true, + _ => false, + }; + + String present(TranslationsEn t) => switch (this) { + Disconnected() => t.home.connection.tapToConnect, + Connecting() => t.home.connection.connecting, + Connected() => t.home.connection.connected, + Disconnecting() => t.home.connection.disconnecting, + }; +} diff --git a/lib/domain/connectivity/connectivity.dart b/lib/domain/connectivity/connectivity.dart new file mode 100644 index 00000000..204a0949 --- /dev/null +++ b/lib/domain/connectivity/connectivity.dart @@ -0,0 +1,4 @@ +export 'connection_status.dart'; +export 'connectivity_failure.dart'; +export 'network_prefs.dart'; +export 'traffic.dart'; diff --git a/lib/domain/connectivity/connectivity_failure.dart b/lib/domain/connectivity/connectivity_failure.dart new file mode 100644 index 00000000..9043f2e2 --- /dev/null +++ b/lib/domain/connectivity/connectivity_failure.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'connectivity_failure.freezed.dart'; + +// TODO: rewrite +@freezed +sealed class ConnectivityFailure with _$ConnectivityFailure, Failure { + const ConnectivityFailure._(); + + const factory ConnectivityFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ConnectivityUnexpectedFailure; + + @override + String present(TranslationsEn t) { + return t.failure.connectivity.unexpected; + } +} diff --git a/lib/domain/connectivity/network_prefs.dart b/lib/domain/connectivity/network_prefs.dart new file mode 100644 index 00000000..6dbb1bb7 --- /dev/null +++ b/lib/domain/connectivity/network_prefs.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'network_prefs.freezed.dart'; +part 'network_prefs.g.dart'; + +@freezed +class NetworkPrefs with _$NetworkPrefs { + const NetworkPrefs._(); + + const factory NetworkPrefs({ + @Default(true) bool systemProxy, + @Default(true) bool bypassPrivateNetworks, + }) = _NetworkPrefs; + + factory NetworkPrefs.fromJson(Map json) => + _$NetworkPrefsFromJson(json); +} diff --git a/lib/domain/connectivity/traffic.dart b/lib/domain/connectivity/traffic.dart new file mode 100644 index 00000000..a7053d53 --- /dev/null +++ b/lib/domain/connectivity/traffic.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'traffic.freezed.dart'; + +@freezed +class Traffic with _$Traffic { + const Traffic._(); + + const factory Traffic({ + required int upload, + required int download, + }) = _Traffic; +} diff --git a/lib/domain/constants.dart b/lib/domain/constants.dart new file mode 100644 index 00000000..cb113d6a --- /dev/null +++ b/lib/domain/constants.dart @@ -0,0 +1,7 @@ +abstract class Constants { + static const localHost = '127.0.0.1'; + static const clashFolderName = "clash"; + static const delayTestUrl = "https://www.google.com"; + static const configFileName = "config"; + static const countryMMDBFileName = "Country"; +} diff --git a/lib/domain/failures.dart b/lib/domain/failures.dart new file mode 100644 index 00000000..3b0b9458 --- /dev/null +++ b/lib/domain/failures.dart @@ -0,0 +1,13 @@ +import 'package:hiddify/core/locale/locale.dart'; + +// TODO: rewrite +mixin Failure { + String present(TranslationsEn t); +} + +extension ErrorPresenter on TranslationsEn { + String presentError(Object error) { + if (error case Failure()) return error.present(this); + return failure.unexpected; + } +} diff --git a/lib/domain/profiles/profile.dart b/lib/domain/profiles/profile.dart new file mode 100644 index 00000000..ac673d11 --- /dev/null +++ b/lib/domain/profiles/profile.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; + +part 'profile.freezed.dart'; +part 'profile.g.dart'; + +@freezed +class Profile with _$Profile { + const Profile._(); + + const factory Profile({ + required String id, + required bool active, + required String name, + required String url, + SubscriptionInfo? subInfo, + Duration? updateInterval, + required DateTime lastUpdate, + }) = _Profile; + + bool get hasSubscriptionInfo => subInfo?.isValid ?? false; + + factory Profile.fromJson(Map json) => + _$ProfileFromJson(json); +} diff --git a/lib/domain/profiles/profiles.dart b/lib/domain/profiles/profiles.dart new file mode 100644 index 00000000..fb00b35b --- /dev/null +++ b/lib/domain/profiles/profiles.dart @@ -0,0 +1,4 @@ +export 'profile.dart'; +export 'profiles_failure.dart'; +export 'profiles_repository.dart'; +export 'subscription_info.dart'; diff --git a/lib/domain/profiles/profiles_failure.dart b/lib/domain/profiles/profiles_failure.dart new file mode 100644 index 00000000..6419a69a --- /dev/null +++ b/lib/domain/profiles/profiles_failure.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'profiles_failure.freezed.dart'; + +@freezed +sealed class ProfileFailure with _$ProfileFailure, Failure { + const ProfileFailure._(); + + const factory ProfileFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ProfileUnexpectedFailure; + + const factory ProfileFailure.notFound() = ProfileNotFoundFailure; + + const factory ProfileFailure.invalidConfig() = ProfileInvalidConfigFailure; + + @override + String present(TranslationsEn t) { + return switch (this) { + ProfileUnexpectedFailure() => t.failure.profiles.unexpected, + ProfileNotFoundFailure() => t.failure.profiles.notFound, + ProfileInvalidConfigFailure() => t.failure.profiles.invalidConfig, + }; + } +} diff --git a/lib/domain/profiles/profiles_repository.dart b/lib/domain/profiles/profiles_repository.dart new file mode 100644 index 00000000..dfb56dc7 --- /dev/null +++ b/lib/domain/profiles/profiles_repository.dart @@ -0,0 +1,20 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; + +abstract class ProfilesRepository { + TaskEither get(String id); + + Stream> watchActiveProfile(); + + Stream> watchHasAnyProfile(); + + Stream>> watchAll(); + + TaskEither add(Profile baseProfile); + + TaskEither update(Profile baseProfile); + + TaskEither setAsActive(String id); + + TaskEither delete(String id); +} diff --git a/lib/domain/profiles/subscription_info.dart b/lib/domain/profiles/subscription_info.dart new file mode 100644 index 00000000..dd8e5e74 --- /dev/null +++ b/lib/domain/profiles/subscription_info.dart @@ -0,0 +1,44 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'subscription_info.freezed.dart'; +part 'subscription_info.g.dart'; + +// TODO: test and improve +@freezed +class SubscriptionInfo with _$SubscriptionInfo { + const SubscriptionInfo._(); + + const factory SubscriptionInfo({ + int? upload, + int? download, + int? total, + @JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) DateTime? expire, + }) = _SubscriptionInfo; + + bool get isValid => + total != null && download != null && upload != null && expire != null; + + bool get isExpired => expire! <= DateTime.now(); + + int get consumption => upload! + download!; + + double get ratio => consumption / total!; + + Duration get remaining => expire!.difference(DateTime.now()); + + factory SubscriptionInfo.fromResponseHeader(String header) { + final values = header.split(';'); + final map = { + for (final v in values) + v.split('=').first: int.tryParse(v.split('=').second) + }; + return SubscriptionInfo.fromJson(map); + } + + factory SubscriptionInfo.fromJson(Map json) => + _$SubscriptionInfoFromJson(json); +} + +DateTime? _dateTimeFromSecondsSinceEpoch(dynamic expire) => + DateTime.fromMillisecondsSinceEpoch((expire as int) * 1000); diff --git a/lib/features/common/active_profile/active_profile_notifier.dart b/lib/features/common/active_profile/active_profile_notifier.dart new file mode 100644 index 00000000..0c232bc7 --- /dev/null +++ b/lib/features/common/active_profile/active_profile_notifier.dart @@ -0,0 +1,30 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'active_profile_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ActiveProfile extends _$ActiveProfile with AppLogger { + @override + Stream build() { + return ref + .watch(profilesRepositoryProvider) + .watchActiveProfile() + .map((event) => event.getOrElse((l) => throw l)); + } + + Future updateProfile() async { + if (state case AsyncData(value: final profile?)) { + loggy.debug("updating active profile"); + return ref + .read(profilesRepositoryProvider) + .update(profile) + .getOrElse((l) => throw l) + .run(); + } + return null; + } +} diff --git a/lib/features/common/active_profile/has_any_profile_notifier.dart b/lib/features/common/active_profile/has_any_profile_notifier.dart new file mode 100644 index 00000000..8ac28b21 --- /dev/null +++ b/lib/features/common/active_profile/has_any_profile_notifier.dart @@ -0,0 +1,14 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'has_any_profile_notifier.g.dart'; + +@Riverpod(keepAlive: true) +Stream hasAnyProfile( + HasAnyProfileRef ref, +) { + return ref + .watch(profilesRepositoryProvider) + .watchHasAnyProfile() + .map((event) => event.getOrElse((l) => throw l)); +} diff --git a/lib/features/common/add_profile_modal.dart b/lib/features/common/add_profile_modal.dart new file mode 100644 index 00000000..6d653695 --- /dev/null +++ b/lib/features/common/add_profile_modal.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class AddProfileModal extends HookConsumerWidget { + const AddProfileModal({ + super.key, + this.scrollController, + }); + + final ScrollController? scrollController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + const buttonsPadding = 24.0; + const buttonsGap = 16.0; + + return SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + LayoutBuilder( + builder: (context, constraints) { + // temporary solution, aspect ratio widget relies on height and in a row there no height! + final buttonWidth = constraints.maxWidth / 2 - + (buttonsPadding + (buttonsGap / 2)); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: buttonsPadding), + child: Row( + children: [ + _Button( + label: t.profile.add.fromClipboard.sentenceCase, + icon: Icons.content_paste, + size: buttonWidth, + onTap: () async { + final captureResult = + await Clipboard.getData(Clipboard.kTextPlain); + final link = + LinkParser.simple(captureResult?.text ?? ''); + if (link != null && context.mounted) { + context.pop(); + await NewProfileRoute(url: link.url, name: link.name) + .push(context); + } else { + CustomToast.error( + t.profile.add.invalidUrlMsg.sentenceCase, + ).show(context); + } + }, + ), + const Gap(buttonsGap), + if (!PlatformUtils.isDesktop) + _Button( + label: t.profile.add.scanQr, + icon: Icons.qr_code_scanner, + size: buttonWidth, + onTap: () async { + final captureResult = + await const QRCodeScannerScreen().open(context); + if (captureResult == null) return; + final link = LinkParser.simple(captureResult); + if (link != null && context.mounted) { + context.pop(); + await NewProfileRoute( + url: link.url, + name: link.name, + ).push(context); + } else { + CustomToast.error( + t.profile.add.invalidUrlMsg.sentenceCase, + ).show(context); + } + }, + ) + else + _Button( + label: t.profile.add.manually.sentenceCase, + icon: Icons.add, + size: buttonWidth, + onTap: () async { + context.pop(); + await const NewProfileRoute().push(context); + }, + ), + ], + ), + ); + }, + ), + if (!PlatformUtils.isDesktop) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: buttonsPadding, + vertical: 16, + ), + child: SizedBox( + height: 36, + child: Material( + elevation: 8, + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + context.pop(); + await const NewProfileRoute().push(context); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Text( + t.profile.add.manually.sentenceCase, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), + ), + const Gap(24), + ], + ), + ); + } +} + +class _Button extends StatelessWidget { + const _Button({ + required this.label, + required this.icon, + required this.size, + required this.onTap, + }); + + final String label; + final IconData icon; + final double size; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.primary; + + return SizedBox( + width: size, + height: size, + child: Material( + elevation: 8, + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: size / 3, + color: color, + ), + const Gap(16), + Flexible( + child: Text( + label, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: color), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/common/clash/clash_controller.dart b/lib/features/common/clash/clash_controller.dart new file mode 100644 index 00000000..abef1944 --- /dev/null +++ b/lib/features/common/clash/clash_controller.dart @@ -0,0 +1,54 @@ +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'clash_controller.g.dart'; + +@Riverpod(keepAlive: true) +class ClashController extends _$ClashController with AppLogger { + Profile? _oldProfile; + + @override + Future build() async { + final clash = ref.watch(clashFacadeProvider); + + final overridesListener = ref.listen( + prefsControllerProvider.select((value) => value.clash), + (_, overrides) async { + loggy.debug("new clash overrides received, patching..."); + await clash.patchOverrides(overrides).getOrElse((l) => throw l).run(); + }, + ); + final overrides = overridesListener.read(); + + final activeProfile = await ref.watch(activeProfileProvider.future); + final oldProfile = _oldProfile; + _oldProfile = activeProfile; + if (activeProfile != null) { + if (oldProfile == null || + oldProfile.id != activeProfile.id || + oldProfile.lastUpdate != activeProfile.lastUpdate) { + loggy.debug("profile changed or updated, updating clash core"); + await clash + .changeConfigs(activeProfile.id) + .call(clash.patchOverrides(overrides)) + .getOrElse((error) { + loggy.warning("failed to change or patch configs, $error"); + throw error; + }).run(); + } + } else { + if (oldProfile != null) { + loggy.debug("active profile removed, resetting clash"); + await clash + .changeConfigs(Constants.configFileName) + .getOrElse((l) => throw l) + .run(); + } + } + } +} diff --git a/lib/features/common/clash/clash_mode.dart b/lib/features/common/clash/clash_mode.dart new file mode 100644 index 00000000..d1e3e848 --- /dev/null +++ b/lib/features/common/clash/clash_mode.dart @@ -0,0 +1,23 @@ +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/features/common/clash/clash_controller.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'clash_mode.g.dart'; + +@Riverpod(keepAlive: true) +class ClashMode extends _$ClashMode with AppLogger { + @override + Future build() async { + final clash = ref.watch(clashFacadeProvider); + await ref.watch(clashControllerProvider.future); + ref.watch(prefsControllerProvider.select((value) => value.clash.mode)); + return clash + .getConfigs() + .map((r) => r.mode) + .getOrElse((l) => throw l) + .run(); + } +} diff --git a/lib/features/common/common.dart b/lib/features/common/common.dart new file mode 100644 index 00000000..23ab52fc --- /dev/null +++ b/lib/features/common/common.dart @@ -0,0 +1,4 @@ +export 'add_profile_modal.dart'; +export 'custom_app_bar.dart'; +export 'qr_code_scanner_screen.dart'; +export 'remaining_traffic_indicator.dart'; diff --git a/lib/features/common/confirmation_dialogs.dart b/lib/features/common/confirmation_dialogs.dart new file mode 100644 index 00000000..2f9f4b11 --- /dev/null +++ b/lib/features/common/confirmation_dialogs.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +Future showConfirmationDialog( + BuildContext context, { + required String title, + required String message, + IconData? icon, +}) async { + return showDialog( + context: context, + builder: (context) { + final localizations = MaterialLocalizations.of(context); + return AlertDialog( + icon: const Icon(Icons.delete_forever), + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => context.pop(true), + child: Text(localizations.okButtonLabel), + ), + TextButton( + onPressed: () => context.pop(false), + child: Text(localizations.cancelButtonLabel), + ), + ], + ); + }, + ).then((value) => value ?? false); +} diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart new file mode 100644 index 00000000..398f41e7 --- /dev/null +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -0,0 +1,62 @@ +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/services/connectivity/connectivity.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connectivity_controller.g.dart'; + +// TODO: test and improve +// TODO: abort connection on clash error +@Riverpod(keepAlive: true) +class ConnectivityController extends _$ConnectivityController with AppLogger { + @override + ConnectionStatus build() { + state = const Disconnected(); + final connection = _connectivity + .watchConnectionStatus() + .map(ConnectionStatus.fromBool) + .listen((event) => state = event); + + // currently changes wont take effect while connected + ref.listen( + prefsControllerProvider.select((value) => value.network), + (_, next) => _networkPrefs = next, + fireImmediately: true, + ); + ref.listen( + prefsControllerProvider + .select((value) => (value.clash.httpPort!, value.clash.socksPort!)), + (_, next) => _ports = (http: next.$1, socks: next.$2), + fireImmediately: true, + ); + + ref.onDispose(connection.cancel); + return state; + } + + ConnectivityService get _connectivity => + ref.watch(connectivityServiceProvider); + + late ({int http, int socks}) _ports; + // ignore: unused_field + late NetworkPrefs _networkPrefs; + + Future toggleConnection() async { + switch (state) { + case Disconnected(): + if (!await _connectivity.grantVpnPermission()) { + state = const Disconnected(ConnectivityFailure.unexpected()); + return; + } + await _connectivity.connect( + httpPort: _ports.http, + socksPort: _ports.socks, + ); + case Connected(): + await _connectivity.disconnect(); + default: + } + } +} diff --git a/lib/features/common/custom_app_bar.dart b/lib/features/common/custom_app_bar.dart new file mode 100644 index 00000000..e08b1000 --- /dev/null +++ b/lib/features/common/custom_app_bar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +abstract class RootScaffold { + static final stateKey = GlobalKey(); +} + +class NestedTabAppBar extends SliverAppBar { + NestedTabAppBar({ + super.key, + super.title, + super.actions, + super.pinned = true, + super.forceElevated, + super.bottom, + }) : super( + leading: RootScaffold.stateKey.currentState?.hasDrawer ?? false + ? DrawerButton( + onPressed: () { + RootScaffold.stateKey.currentState?.openDrawer(); + }, + ) + : null, + ); +} diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart new file mode 100644 index 00000000..11fc5dc2 --- /dev/null +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class QRCodeScannerScreen extends HookConsumerWidget with PresLogger { + const QRCodeScannerScreen({super.key}); + + Future open(BuildContext context) async { + return Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => const QRCodeScannerScreen(), + ), + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useMemoized( + () => MobileScannerController( + detectionSpeed: DetectionSpeed.noDuplicates, + formats: [BarcodeFormat.qrCode], + ), + ); + + useEffect(() => controller.dispose, []); + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + iconTheme: Theme.of(context).iconTheme.copyWith( + color: Colors.white, + size: 32, + ), + actions: [ + IconButton( + icon: ValueListenableBuilder( + valueListenable: controller.torchState, + builder: (context, state, child) { + switch (state) { + case TorchState.off: + return const Icon(Icons.flash_off, color: Colors.grey); + case TorchState.on: + return const Icon(Icons.flash_on, color: Colors.yellow); + } + }, + ), + onPressed: () => controller.toggleTorch(), + ), + IconButton( + icon: ValueListenableBuilder( + valueListenable: controller.cameraFacingState, + builder: (context, state, child) { + switch (state) { + case CameraFacing.front: + return const Icon(Icons.camera_front); + case CameraFacing.back: + return const Icon(Icons.camera_rear); + } + }, + ), + onPressed: () => controller.switchCamera(), + ), + ], + ), + body: MobileScanner( + controller: controller, + onDetect: (capture) { + final data = capture.barcodes.first; + if (context.mounted && data.type == BarcodeType.url) { + loggy.debug('captured raw: [${data.rawValue}]'); + loggy.debug('captured url: [${data.url?.url}]'); + Navigator.of(context, rootNavigator: true).pop(data.url?.url); + } + }, + ), + ); + } +} diff --git a/lib/features/common/remaining_traffic_indicator.dart b/lib/features/common/remaining_traffic_indicator.dart new file mode 100644 index 00000000..10de1ce7 --- /dev/null +++ b/lib/features/common/remaining_traffic_indicator.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:percent_indicator/percent_indicator.dart'; + +// TODO: change colors +class RemainingTrafficIndicator extends StatelessWidget { + const RemainingTrafficIndicator(this.ratio, {super.key}); + + final double ratio; + + @override + Widget build(BuildContext context) { + final startColor = ratio < 0.25 + ? const Color.fromRGBO(93, 205, 251, 1.0) + : ratio < 0.65 + ? const Color.fromRGBO(205, 199, 64, 1.0) + : const Color.fromRGBO(241, 82, 81, 1.0); + final endColor = ratio < 0.25 + ? const Color.fromRGBO(49, 146, 248, 1.0) + : ratio < 0.65 + ? const Color.fromRGBO(98, 115, 32, 1.0) + : const Color.fromRGBO(139, 30, 36, 1.0); + + return LinearPercentIndicator( + percent: ratio, + animation: true, + padding: EdgeInsets.zero, + lineHeight: 6, + barRadius: const Radius.circular(16), + linearGradient: LinearGradient( + colors: [startColor, endColor], + ), + ); + } +} diff --git a/lib/features/common/traffic/traffic_chart.dart b/lib/features/common/traffic/traffic_chart.dart new file mode 100644 index 00000000..b799f44f --- /dev/null +++ b/lib/features/common/traffic/traffic_chart.dart @@ -0,0 +1,94 @@ +import 'package:dartx/dartx.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/features/common/traffic/traffic_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +// TODO: test implementation, rewrite +class TrafficChart extends HookConsumerWidget { + const TrafficChart({ + super.key, + this.chartSteps = 20, + }); + + final int chartSteps; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncTraffics = ref.watch(trafficNotifierProvider); + + switch (asyncTraffics) { + case AsyncData(value: final traffics): + final latest = + traffics.lastOrNull ?? const Traffic(upload: 0, download: 0); + final latestUploadData = formatByteSpeed(latest.upload); + final latestDownloadData = formatByteSpeed(latest.download); + + final uploadChartSpots = traffics.takeLast(chartSteps).mapIndexed( + (index, p) => FlSpot(index.toDouble(), p.upload.toDouble()), + ); + final downloadChartSpots = traffics.takeLast(chartSteps).mapIndexed( + (index, p) => FlSpot(index.toDouble(), p.download.toDouble()), + ); + + return Column( + mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 68, + child: LineChart( + LineChartData( + minY: 0, + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + gridData: const FlGridData(show: false), + lineTouchData: const LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + isCurved: true, + preventCurveOverShooting: true, + dotData: const FlDotData(show: false), + spots: uploadChartSpots.toList(), + ), + LineChartBarData( + color: Theme.of(context).colorScheme.tertiary, + isCurved: true, + preventCurveOverShooting: true, + dotData: const FlDotData(show: false), + spots: downloadChartSpots.toList(), + ), + ], + ), + duration: Duration.zero, + ), + ), + const Gap(16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("↑"), + Text(latestUploadData.size), + Text(latestUploadData.unit), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("↓"), + Text(latestDownloadData.size), + Text(latestDownloadData.unit), + ], + ), + const Gap(16), + ], + ); + // TODO: handle loading and error + default: + return const SizedBox(); + } + } +} diff --git a/lib/features/common/traffic/traffic_notifier.dart b/lib/features/common/traffic/traffic_notifier.dart new file mode 100644 index 00000000..985f1ff0 --- /dev/null +++ b/lib/features/common/traffic/traffic_notifier.dart @@ -0,0 +1,40 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'traffic_notifier.g.dart'; + +// TODO: improve +@riverpod +class TrafficNotifier extends _$TrafficNotifier with AppLogger { + int get _steps => 100; + + @override + Stream> build() { + return Stream.periodic(const Duration(seconds: 1)).asyncMap( + (_) async { + return ref.read(clashFacadeProvider).getTraffic().match( + (f) { + loggy.warning('failed to watch clash traffic: $f'); + return const ClashTraffic(upload: 0, download: 0); + }, + (traffic) => traffic, + ).run(); + }, + ).map( + (event) => switch (state) { + AsyncData(:final value) => [ + ...value.takeLast(_steps - 1), + Traffic(upload: event.upload, download: event.download), + ], + _ => List.generate( + _steps, + (index) => const Traffic(upload: 0, download: 0), + ) + }, + ); + } +} diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart new file mode 100644 index 00000000..e2399235 --- /dev/null +++ b/lib/features/home/view/home_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart'; +import 'package:hiddify/features/common/clash/clash_controller.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/home/widgets/widgets.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +class HomePage extends HookConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final hasAnyProfile = ref.watch(hasAnyProfileProvider); + final activeProfile = ref.watch(activeProfileProvider); + + ref.listen( + clashControllerProvider, + (_, next) { + if (next case AsyncError(:final error)) { + CustomToast.error( + t.presentError(error), + duration: const Duration(seconds: 10), + ).show(context); + } + }, + ); + + return Scaffold( + body: Stack( + alignment: Alignment.bottomCenter, + children: [ + CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.general.appTitle.titleCase), + actions: [ + IconButton( + onPressed: () => const AddProfileRoute().push(context), + icon: const Icon(Icons.add_circle), + ), + ], + ), + switch (activeProfile) { + AsyncData(value: final profile?) => MultiSliver( + children: [ + ActiveProfileCard(profile), + const SliverFillRemaining( + hasScrollBody: false, + child: Padding( + padding: EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 86, + ), + child: ConnectionButton(), + ), + ), + ], + ), + AsyncData() => switch (hasAnyProfile) { + AsyncData(value: true) => + const EmptyActiveProfileHomeBody(), + _ => const EmptyProfilesHomeBody(), + }, + AsyncError(:final error) => + SliverErrorBodyPlaceholder(t.presentError(error)), + _ => const SliverToBoxAdapter(), + }, + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/home/view/view.dart b/lib/features/home/view/view.dart new file mode 100644 index 00000000..e4ff2696 --- /dev/null +++ b/lib/features/home/view/view.dart @@ -0,0 +1 @@ +export 'home_page.dart'; diff --git a/lib/features/home/widgets/active_profile_card.dart b/lib/features/home/widgets/active_profile_card.dart new file mode 100644 index 00000000..0aea5235 --- /dev/null +++ b/lib/features/home/widgets/active_profile_card.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +// TODO: rewrite +class ActiveProfileCard extends HookConsumerWidget { + const ActiveProfileCard(this.profile, {super.key}); + + final Profile profile; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Material( + borderRadius: BorderRadius.circular(16), + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + await const ProfilesRoute().push(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Row( + children: [ + Expanded( + child: Text( + profile.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const Gap(4), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + ), + ), + TextButton.icon( + onPressed: () async { + const AddProfileRoute().push(context); + }, + label: Text(t.profile.add.buttonText.titleCase), + icon: const Icon(Icons.add), + ), + ], + ), + if (profile.hasSubscriptionInfo) ...[ + const Divider(thickness: 0.5), + SubscriptionInfoTile(profile.subInfo!), + ], + ], + ), + ), + ); + } +} + +class SubscriptionInfoTile extends HookConsumerWidget { + const SubscriptionInfoTile(this.subInfo, {super.key}); + + final SubscriptionInfo subInfo; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (!subInfo.isValid) return const SizedBox.shrink(); + final t = ref.watch(translationsProvider); + + final themeData = Theme.of(context); + + final updateProfileMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + initialOnSuccess: () => + CustomToast.success(t.profile.update.successMsg).show(context), + ); + + return Row( + children: [ + Flexible( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + formatTrafficByteSize( + subInfo.consumption, + subInfo.total!, + ), + style: themeData.textTheme.titleSmall, + ), + ), + Text( + t.profile.subscription.traffic, + style: themeData.textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 2), + RemainingTrafficIndicator(subInfo.ratio), + ], + ), + ), + const Gap(8), + IconButton( + onPressed: () async { + if (updateProfileMutation.state.isInProgress) return; + updateProfileMutation.setFuture( + ref.read(activeProfileProvider.notifier).updateProfile(), + ); + }, + icon: const Icon(Icons.refresh, size: 44), + ), + const Gap(8), + if (subInfo.isExpired) + Text( + t.profile.subscription.expired, + style: themeData.textTheme.titleSmall + ?.copyWith(color: themeData.colorScheme.error), + ) + else if (subInfo.ratio >= 1) + Text( + t.profile.subscription.noTraffic, + style: themeData.textTheme.titleSmall + ?.copyWith(color: themeData.colorScheme.error), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formatExpireDuration(subInfo.remaining), + style: themeData.textTheme.titleSmall, + ), + Text( + t.profile.subscription.remaining, + style: themeData.textTheme.bodySmall, + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/home/widgets/connection_button.dart b/lib/features/home/widgets/connection_button.dart new file mode 100644 index 00000000..4d93ad78 --- /dev/null +++ b/lib/features/home/widgets/connection_button.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/theme/theme.dart'; +import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +// TODO: rewrite +class ConnectionButton extends HookConsumerWidget { + const ConnectionButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final connectionStatus = ref.watch(connectivityControllerProvider); + + final Color connectionLogoColor = connectionStatus.isConnected + ? ConnectionButtonColor.connected + : ConnectionButtonColor.disconnected; + + final bool intractable = !connectionStatus.isSwitching; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + blurRadius: 16, + color: connectionLogoColor.withOpacity(0.5), + ), + ], + ), + width: 148, + height: 148, + child: Material( + shape: const CircleBorder(), + color: Colors.white, + child: InkWell( + onTap: () async { + await ref + .read(connectivityControllerProvider.notifier) + .toggleConnection(); + }, + child: Padding( + padding: const EdgeInsets.all(36), + child: Assets.images.logo.svg( + colorFilter: ColorFilter.mode( + connectionLogoColor, + BlendMode.srcIn, + ), + ), + ), + ), + ).animate(target: intractable ? 0 : 1).blurXY(end: 1), + ).animate(target: intractable ? 0 : 1).scaleXY(end: .88), + const Gap(16), + Text( + connectionStatus.present(t), + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ); + } +} diff --git a/lib/features/home/widgets/empty_profiles_home_body.dart b/lib/features/home/widgets/empty_profiles_home_body.dart new file mode 100644 index 00000000..16215d62 --- /dev/null +++ b/lib/features/home/widgets/empty_profiles_home_body.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class EmptyProfilesHomeBody extends HookConsumerWidget { + const EmptyProfilesHomeBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(t.home.emptyProfilesMsg.sentenceCase), + const Gap(16), + OutlinedButton.icon( + onPressed: () => const AddProfileRoute().push(context), + icon: const Icon(Icons.add), + label: Text(t.profile.add.buttonText.titleCase), + ) + ], + ), + ); + } +} + +class EmptyActiveProfileHomeBody extends HookConsumerWidget { + const EmptyActiveProfileHomeBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(t.home.noActiveProfileMsg.sentenceCase), + const Gap(16), + OutlinedButton( + onPressed: () => const ProfilesRoute().push(context), + child: Text(t.profile.overviewPageTitle.titleCase), + ) + ], + ), + ); + } +} diff --git a/lib/features/home/widgets/widgets.dart b/lib/features/home/widgets/widgets.dart new file mode 100644 index 00000000..ef518560 --- /dev/null +++ b/lib/features/home/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'active_profile_card.dart'; +export 'connection_button.dart'; +export 'empty_profiles_home_body.dart'; diff --git a/lib/features/logs/notifier/logs_notifier.dart b/lib/features/logs/notifier/logs_notifier.dart new file mode 100644 index 00000000..e3fce2d2 --- /dev/null +++ b/lib/features/logs/notifier/logs_notifier.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/features/logs/notifier/logs_state.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'logs_notifier.g.dart'; + +// TODO: rewrite +@riverpod +class LogsNotifier extends _$LogsNotifier with AppLogger { + static const maxLength = 1000; + + @override + Stream build() { + state = const AsyncData(LogsState()); + return ref.read(clashFacadeProvider).watchLogs().asyncMap( + (event) async { + _logs = [ + event.getOrElse((l) => throw l), + ..._logs.takeFirst(maxLength - 1), + ]; + return switch (state) { + // ignore: unused_result + AsyncData(:final value) => value.copyWith(logs: await _computeLogs()), + _ => LogsState(logs: await _computeLogs()), + }; + }, + ); + } + + var _logs = []; + final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200)); + LogLevel? _levelFilter; + String _filter = ""; + + Future> _computeLogs() async { + if (_levelFilter == null && _filter.isEmpty) return _logs; + return _logs.where((e) { + return (_filter.isEmpty || e.message.contains(_filter)) && + (_levelFilter == null || e.level == _levelFilter); + }).toList(); + } + + void clear() { + if (state case AsyncData(:final value)) { + state = AsyncData(value.copyWith(logs: [])).copyWithPrevious(state); + } + } + + void filterMessage(String? filter) { + _filter = filter ?? ''; + _debouncer( + () async { + if (state case AsyncData(:final value)) { + state = AsyncData( + value.copyWith( + filter: _filter, + logs: await _computeLogs(), + ), + ).copyWithPrevious(state); + } + }, + ); + } + + Future filterLevel(LogLevel? level) async { + _levelFilter = level; + if (state case AsyncData(:final value)) { + state = AsyncData( + value.copyWith( + levelFilter: _levelFilter, + logs: await _computeLogs(), + ), + ).copyWithPrevious(state); + } + } +} diff --git a/lib/features/logs/notifier/logs_state.dart b/lib/features/logs/notifier/logs_state.dart new file mode 100644 index 00000000..dbed8842 --- /dev/null +++ b/lib/features/logs/notifier/logs_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash.dart'; + +part 'logs_state.freezed.dart'; + +@freezed +class LogsState with _$LogsState { + const LogsState._(); + + const factory LogsState({ + @Default([]) List logs, + @Default("") String filter, + LogLevel? levelFilter, + }) = _LogsState; +} diff --git a/lib/features/logs/notifier/notifier.dart b/lib/features/logs/notifier/notifier.dart new file mode 100644 index 00000000..69135d86 --- /dev/null +++ b/lib/features/logs/notifier/notifier.dart @@ -0,0 +1,2 @@ +export 'logs_notifier.dart'; +export 'logs_state.dart'; diff --git a/lib/features/logs/view/logs_page.dart b/lib/features/logs/view/logs_page.dart new file mode 100644 index 00000000..060731ee --- /dev/null +++ b/lib/features/logs/view/logs_page.dart @@ -0,0 +1,138 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/logs/notifier/notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class LogsPage extends HookConsumerWidget { + const LogsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final asyncState = ref.watch(logsNotifierProvider); + final notifier = ref.watch(logsNotifierProvider.notifier); + + switch (asyncState) { + case AsyncData(value: final state): + return Scaffold( + appBar: AppBar( + // TODO: fix height + toolbarHeight: 90, + title: Text(t.logs.pageTitle.titleCase), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(36), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Flexible( + child: TextFormField( + onChanged: notifier.filterMessage, + decoration: InputDecoration( + isDense: true, + hintText: t.logs.filterHint.sentenceCase, + ), + ), + ), + const Gap(16), + DropdownButton>( + value: optionOf(state.levelFilter), + onChanged: (v) { + if (v == null) return; + notifier.filterLevel(v.toNullable()); + }, + padding: const EdgeInsets.symmetric(horizontal: 8), + borderRadius: BorderRadius.circular(4), + items: [ + DropdownMenuItem( + value: none(), + child: Text(t.logs.allLevelsFilter.sentenceCase), + ), + ...LogLevel.values.takeFirst(3).map( + (e) => DropdownMenuItem( + value: some(e), + child: Text(e.name.sentenceCase), + ), + ), + ], + ), + ], + ), + ), + ), + ), + body: ListView.builder( + itemCount: state.logs.length, + reverse: true, + itemBuilder: (context, index) { + final log = state.logs[index]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + title: Text.rich( + TextSpan( + children: [ + TextSpan(text: log.timeStamp), + const TextSpan(text: " "), + TextSpan( + text: log.level.name.toUpperCase(), + style: TextStyle(color: log.level.color), + ), + ], + ), + ), + subtitle: Text(log.message), + ), + if (index != 0) + const Divider( + indent: 16, + endIndent: 16, + height: 4, + ), + ], + ); + }, + ), + ); + + case AsyncError(:final error): + return Scaffold( + body: CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.logs.pageTitle.titleCase), + ), + SliverErrorBodyPlaceholder(t.presentError(error)), + ], + ), + ); + + case AsyncLoading(): + return Scaffold( + body: CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.logs.pageTitle.titleCase), + ), + const SliverLoadingBodyPlaceholder(), + ], + ), + ); + + // TODO: remove + default: + return const Scaffold(); + } + } +} diff --git a/lib/features/logs/view/view.dart b/lib/features/logs/view/view.dart new file mode 100644 index 00000000..2982c609 --- /dev/null +++ b/lib/features/logs/view/view.dart @@ -0,0 +1 @@ +export 'logs_page.dart'; diff --git a/lib/features/profile_detail/notifier/notifier.dart b/lib/features/profile_detail/notifier/notifier.dart new file mode 100644 index 00000000..a6381143 --- /dev/null +++ b/lib/features/profile_detail/notifier/notifier.dart @@ -0,0 +1,2 @@ +export 'profile_detail_notifier.dart'; +export 'profile_detail_state.dart'; diff --git a/lib/features/profile_detail/notifier/profile_detail_notifier.dart b/lib/features/profile_detail/notifier/profile_detail_notifier.dart new file mode 100644 index 00000000..a24dff2e --- /dev/null +++ b/lib/features/profile_detail/notifier/profile_detail_notifier.dart @@ -0,0 +1,113 @@ +import 'package:dartx/dartx.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/profile_detail/notifier/profile_detail_state.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:uuid/uuid.dart'; + +part 'profile_detail_notifier.g.dart'; + +@riverpod +class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { + @override + Future build( + String id, { + String? url, + String? name, + }) async { + if (id == 'new') { + return ProfileDetailState( + profile: Profile( + id: const Uuid().v4(), + active: true, + name: name ?? "", + url: url ?? "", + lastUpdate: DateTime.now(), + ), + ); + } + final failureOrProfile = await _profilesRepo.get(id).run(); + return failureOrProfile.match( + (l) { + loggy.warning('failed to load profile, $l'); + throw l; + }, + (profile) { + if (profile == null) { + loggy.warning('profile with id: [$id] does not exist'); + throw const ProfileNotFoundFailure(); + } + return ProfileDetailState(profile: profile, isEditing: true); + }, + ); + } + + ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); + + void setField({String? name, String? url}) { + if (state case AsyncData(:final value)) { + state = AsyncData( + value.copyWith( + profile: value.profile.copyWith( + name: name ?? value.profile.name, + url: url ?? value.profile.url, + ), + ), + ).copyWithPrevious(state); + } + } + + Future save() async { + if (state case AsyncData(:final value)) { + if (value.save.isInProgress) return; + final profile = value.profile; + loggy.debug( + 'saving profile, url: [${profile.url}], name: [${profile.name}]', + ); + state = AsyncData(value.copyWith(save: const MutationInProgress())) + .copyWithPrevious(state); + Either? failureOrSuccess; + if (profile.name.isBlank || profile.url.isBlank) { + loggy.debug('profile save: invalid arguments'); + } else if (value.isEditing) { + loggy.debug('updating profile'); + failureOrSuccess = await _profilesRepo.update(profile).run(); + } else { + loggy.debug('adding profile, url: [${profile.url}]'); + failureOrSuccess = await _profilesRepo.add(profile).run(); + } + state = AsyncData( + value.copyWith( + save: failureOrSuccess?.fold( + (l) => MutationFailure(l), + (_) => const MutationSuccess(), + ) ?? + value.save, + showErrorMessages: true, + ), + ).copyWithPrevious(state); + } + } + + Future delete() async { + if (state case AsyncData(:final value)) { + if (value.delete.isInProgress) return; + final profile = value.profile; + loggy.debug('deleting profile'); + state = AsyncData( + value.copyWith(delete: const MutationState.inProgress()), + ).copyWithPrevious(state); + final result = await _profilesRepo.delete(profile.id).run(); + state = AsyncData( + value.copyWith( + delete: result.match( + (l) => MutationFailure(l), + (_) => const MutationSuccess(), + ), + ), + ).copyWithPrevious(state); + } + } +} diff --git a/lib/features/profile_detail/notifier/profile_detail_state.dart b/lib/features/profile_detail/notifier/profile_detail_state.dart new file mode 100644 index 00000000..d4b96ac3 --- /dev/null +++ b/lib/features/profile_detail/notifier/profile_detail_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/utils/utils.dart'; + +part 'profile_detail_state.freezed.dart'; + +@freezed +class ProfileDetailState with _$ProfileDetailState { + const ProfileDetailState._(); + + const factory ProfileDetailState({ + required Profile profile, + @Default(false) bool isEditing, + @Default(false) bool showErrorMessages, + @Default(MutationState.initial()) MutationState save, + @Default(MutationState.initial()) MutationState delete, + }) = _ProfileDetailState; + + bool get isBusy => + (save.isInProgress || save is MutationSuccess) || + (delete.isInProgress || delete is MutationSuccess); +} diff --git a/lib/features/profile_detail/view/profile_detail_page.dart b/lib/features/profile_detail/view/profile_detail_page.dart new file mode 100644 index 00000000..658a8a4c --- /dev/null +++ b/lib/features/profile_detail/view/profile_detail_page.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/confirmation_dialogs.dart'; +import 'package:hiddify/features/profile_detail/notifier/notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +// TODO: test and improve +// TODO: prevent popping screen when busy +class ProfileDetailPage extends HookConsumerWidget with PresLogger { + const ProfileDetailPage( + this.id, { + super.key, + this.url, + this.name, + }); + + final String id; + final String? url; + final String? name; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = profileDetailNotifierProvider(id, url: url, name: name); + final t = ref.watch(translationsProvider); + final asyncState = ref.watch(provider); + final notifier = ref.watch(provider.notifier); + + final themeData = Theme.of(context); + + ref.listen( + provider.select((data) => data.whenData((value) => value.save)), + (_, asyncSave) { + if (asyncSave case AsyncData(value: final save)) { + switch (save) { + case MutationFailure(:final failure): + CustomToast.error(t.presentError(failure)).show(context); + case MutationSuccess(): + CustomToast.success(t.profile.save.successMsg.sentenceCase) + .show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + } + } + }, + ); + + ref.listen( + provider.select((data) => data.whenData((value) => value.delete)), + (_, asyncSave) { + if (asyncSave case AsyncData(value: final delete)) { + switch (delete) { + case MutationFailure(:final failure): + CustomToast.error(t.presentError(failure)).show(context); + case MutationSuccess(): + CustomToast.success(t.profile.delete.successMsg.sentenceCase) + .show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + } + } + }, + ); + + switch (asyncState) { + case AsyncData(value: final state): + return Stack( + children: [ + Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(t.profile.detailsPageTitle.titleCase), + ), + const SliverGap(8), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: Form( + autovalidateMode: state.showErrorMessages + ? AutovalidateMode.always + : AutovalidateMode.disabled, + child: SliverList( + delegate: SliverChildListDelegate( + [ + const Gap(8), + CustomTextFormField( + initialValue: state.profile.name, + onChanged: (value) => + notifier.setField(name: value), + validator: (value) => (value?.isEmpty ?? true) + ? t.profile.detailsForm.emptyNameMsg + : null, + label: t.profile.detailsForm.nameHint.titleCase, + ), + const Gap(16), + CustomTextFormField( + initialValue: state.profile.url, + onChanged: (value) => + notifier.setField(url: value), + validator: (value) => + (value != null && !isUrl(value)) + ? t.profile.detailsForm.invalidUrlMsg + : null, + label: + t.profile.detailsForm.urlHint.toUpperCase(), + ), + ], + ), + ), + ), + ), + SliverFillRemaining( + hasScrollBody: false, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + OverflowBar( + spacing: 12, + overflowAlignment: OverflowBarAlignment.end, + children: [ + if (state.isEditing) + FilledButton( + onPressed: () async { + final deleteConfirmed = + await showConfirmationDialog( + context, + title: + t.profile.delete.buttonText.titleCase, + message: t.profile.delete.confirmationMsg + .sentenceCase, + ); + if (deleteConfirmed) { + await notifier.delete(); + } + }, + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + themeData.colorScheme.errorContainer, + ), + ), + child: Text( + t.profile.delete.buttonText.titleCase, + style: TextStyle( + color: themeData + .colorScheme.onErrorContainer, + ), + ), + ), + OutlinedButton( + onPressed: notifier.save, + child: + Text(t.profile.save.buttonText.titleCase), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + if (state.isBusy) + Positioned.fill( + child: Container( + color: Colors.black54, + padding: const EdgeInsets.symmetric(horizontal: 36), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LinearProgressIndicator( + backgroundColor: Colors.transparent, + ), + ], + ), + ), + ), + ], + ); + + // TODO: handle loading and error states + default: + return const Scaffold(); + } + } +} diff --git a/lib/features/profile_detail/view/view.dart b/lib/features/profile_detail/view/view.dart new file mode 100644 index 00000000..bcb57dd1 --- /dev/null +++ b/lib/features/profile_detail/view/view.dart @@ -0,0 +1 @@ +export 'profile_detail_page.dart'; diff --git a/lib/features/profiles/notifier/notifier.dart b/lib/features/profiles/notifier/notifier.dart new file mode 100644 index 00000000..3e29cfc6 --- /dev/null +++ b/lib/features/profiles/notifier/notifier.dart @@ -0,0 +1 @@ +export 'profiles_notifier.dart'; diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart new file mode 100644 index 00000000..a2ca2a60 --- /dev/null +++ b/lib/features/profiles/notifier/profiles_notifier.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profiles_notifier.g.dart'; + +@riverpod +class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { + @override + Stream> build() { + return _profilesRepo + .watchAll() + .map((event) => event.getOrElse((l) => throw l)); + } + + ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); + + Future selectActiveProfile(String id) async { + loggy.debug('changing active profile to: [$id]'); + await _profilesRepo.setAsActive(id).mapLeft((f) { + loggy.warning('failed to set [$id] as active profile, $f'); + throw f; + }).run(); + } + + Future deleteProfile(Profile profile) async { + loggy.debug('deleting profile: ${profile.name}'); + await _profilesRepo.delete(profile.id).mapLeft( + (f) { + loggy.warning('failed to delete profile, $f'); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/profiles/view/profiles_modal.dart b/lib/features/profiles/view/profiles_modal.dart new file mode 100644 index 00000000..739a7609 --- /dev/null +++ b/lib/features/profiles/view/profiles_modal.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profiles/widgets/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class ProfilesModal extends HookConsumerWidget { + const ProfilesModal({ + super.key, + this.scrollController, + }); + + final ScrollController? scrollController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncProfiles = ref.watch(profilesNotifierProvider); + + return Scaffold( + backgroundColor: Colors.transparent, + body: CustomScrollView( + controller: scrollController, + slivers: [ + switch (asyncProfiles) { + AsyncData(value: final profiles) => SliverList.builder( + itemBuilder: (context, index) { + final profile = profiles[index]; + return ProfileTile(profile); + }, + itemCount: profiles.length, + ), + // TODO: handle loading and error + _ => const SliverToBoxAdapter(), + }, + ], + ), + ); + } +} diff --git a/lib/features/profiles/view/view.dart b/lib/features/profiles/view/view.dart new file mode 100644 index 00000000..fa509805 --- /dev/null +++ b/lib/features/profiles/view/view.dart @@ -0,0 +1 @@ +export 'profiles_modal.dart'; diff --git a/lib/features/profiles/widgets/profile_tile.dart b/lib/features/profiles/widgets/profile_tile.dart new file mode 100644 index 00000000..f02da509 --- /dev/null +++ b/lib/features/profiles/widgets/profile_tile.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/common/confirmation_dialogs.dart'; +import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class ProfileTile extends HookConsumerWidget { + const ProfileTile(this.profile, {super.key}); + + final Profile profile; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final subInfo = profile.subInfo; + + final themeData = Theme.of(context); + + final selectActiveMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + ); + final deleteProfileMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + ); + + return Card( + elevation: 6, + clipBehavior: Clip.antiAlias, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + shadowColor: Colors.transparent, + color: profile.active ? themeData.colorScheme.tertiaryContainer : null, + child: InkWell( + onTap: () { + if (profile.active || selectActiveMutation.state.isInProgress) return; + selectActiveMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .selectActiveProfile(profile.id), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text.rich( + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + TextSpan( + text: profile.name, + style: themeData.textTheme.titleMedium, + ), + const TextSpan(text: " • "), + TextSpan( + text: t.profile.subscription.updatedTimeAgo( + timeago: timeago.format(profile.lastUpdate), + ), + ), + ], + ), + ), + ), + Row( + children: [ + const Gap(12), + SizedBox( + width: 18, + height: 18, + child: IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + iconSize: 18, + onPressed: () async { + // await context.push(Routes.profile(profile.id).path); + // TODO: temp + await ProfileDetailsRoute(profile.id).push(context); + }, + ), + ), + const Gap(12), + SizedBox( + width: 18, + height: 18, + child: IconButton( + icon: const Icon(Icons.delete_forever), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + iconSize: 18, + onPressed: () async { + if (deleteProfileMutation.state.isInProgress) { + return; + } + final deleteConfirmed = + await showConfirmationDialog( + context, + title: t.profile.delete.buttonText.titleCase, + message: + t.profile.delete.confirmationMsg.sentenceCase, + ); + if (deleteConfirmed) { + deleteProfileMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .deleteProfile(profile), + ); + } + }, + ), + ), + ], + ), + ], + ), + if (subInfo?.isValid ?? false) ...[ + const Gap(2), + Row( + children: [ + if (subInfo!.isExpired) + Text( + t.profile.subscription.expired, + style: themeData.textTheme.titleSmall + ?.copyWith(color: themeData.colorScheme.error), + ) + else if (subInfo.ratio >= 1) + Text( + t.profile.subscription.noTraffic, + style: themeData.textTheme.titleSmall?.copyWith( + color: themeData.colorScheme.error, + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formatExpireDuration(subInfo.remaining), + style: themeData.textTheme.titleSmall, + ), + Text( + t.profile.subscription.remaining, + style: themeData.textTheme.bodySmall, + ), + ], + ), + const Gap(16), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + formatTrafficByteSize( + subInfo.consumption, + subInfo.total!, + ), + style: themeData.textTheme.titleMedium, + ), + RemainingTrafficIndicator(subInfo.ratio), + ], + ), + ), + ], + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/profiles/widgets/widgets.dart b/lib/features/profiles/widgets/widgets.dart new file mode 100644 index 00000000..9d452d84 --- /dev/null +++ b/lib/features/profiles/widgets/widgets.dart @@ -0,0 +1 @@ +export 'profile_tile.dart'; diff --git a/lib/features/proxies/model/group_with_proxies.dart b/lib/features/proxies/model/group_with_proxies.dart new file mode 100644 index 00000000..e0128a44 --- /dev/null +++ b/lib/features/proxies/model/group_with_proxies.dart @@ -0,0 +1,43 @@ +import 'package:combine/combine.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/clash/clash.dart'; + +part 'group_with_proxies.freezed.dart'; + +@freezed +class GroupWithProxies with _$GroupWithProxies { + const GroupWithProxies._(); + + const factory GroupWithProxies({ + required ClashProxyGroup group, + required List proxies, + }) = _GroupWithProxies; + + static Future> fromProxies( + List proxies, + TunnelMode? mode, + ) async { + final stopWatch = Stopwatch()..start(); + final res = await CombineWorker().execute( + () { + final result = []; + for (final proxy in proxies) { + if (proxy is ClashProxyGroup) { + if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue; + final current = []; + for (final name in proxy.all) { + current.addAll(proxies.where((e) => e.name == name).toList()); + } + result.add(GroupWithProxies(group: proxy, proxies: current)); + } + } + return result; + }, + ); + debugPrint( + "computed grouped proxies in [${stopWatch.elapsedMilliseconds}ms]", + ); + return res; + } +} diff --git a/lib/features/proxies/model/model.dart b/lib/features/proxies/model/model.dart new file mode 100644 index 00000000..2b85f91f --- /dev/null +++ b/lib/features/proxies/model/model.dart @@ -0,0 +1 @@ +export 'group_with_proxies.dart'; diff --git a/lib/features/proxies/notifier/notifier.dart b/lib/features/proxies/notifier/notifier.dart new file mode 100644 index 00000000..60e74984 --- /dev/null +++ b/lib/features/proxies/notifier/notifier.dart @@ -0,0 +1 @@ +export 'proxies_notifier.dart'; diff --git a/lib/features/proxies/notifier/proxies_delay_notifier.dart b/lib/features/proxies/notifier/proxies_delay_notifier.dart new file mode 100644 index 00000000..e36cabcc --- /dev/null +++ b/lib/features/proxies/notifier/proxies_delay_notifier.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'proxies_delay_notifier.g.dart'; + +// TODO: rewrite +@Riverpod(keepAlive: true) +class ProxiesDelayNotifier extends _$ProxiesDelayNotifier with AppLogger { + @override + Map build() { + ref.onDispose( + () { + loggy.debug("disposing"); + _currentTest?.cancel(); + }, + ); + + ref.listen( + activeProfileProvider.selectAsync((value) => value?.id), + (prev, next) async { + if (await prev != await next) ref.invalidateSelf(); + }, + ); + + return {}; + } + + ClashFacade get _clash => ref.read(clashFacadeProvider); + StreamSubscription? _currentTest; + + Future testDelay(Iterable proxies) async { + loggy.debug('testing delay for [${proxies.length}] proxies'); + + // cancel possible running test + await _currentTest?.cancel(); + + // reset previous + state = state.filterNot((entry) => proxies.contains(entry.key)); + + void setDelay(String name, int delay) { + state = { + ...state + ..update( + name, + (_) => delay, + ifAbsent: () => delay, + ) + }; + } + + _currentTest = Stream.fromIterable(proxies) + .bufferCount(5) + .asyncMap( + (chunk) => Future.wait( + chunk.map( + (e) async => setDelay( + e, + await _clash.testDelay(e).getOrElse((l) => -1).run(), + ), + ), + ), + ) + .listen((event) {}); + } + + Future cancelDelayTest() async => _currentTest?.cancel(); +} diff --git a/lib/features/proxies/notifier/proxies_notifier.dart b/lib/features/proxies/notifier/proxies_notifier.dart new file mode 100644 index 00000000..5fb4da77 --- /dev/null +++ b/lib/features/proxies/notifier/proxies_notifier.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/features/common/clash/clash_controller.dart'; +import 'package:hiddify/features/common/clash/clash_mode.dart'; +import 'package:hiddify/features/proxies/model/model.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'proxies_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { + @override + Future> build() async { + loggy.debug('building'); + await ref.watch(clashControllerProvider.future); + final mode = await ref.watch(clashModeProvider.future); + return _clash + .getProxies() + .flatMap( + (proxies) { + return TaskEither( + () async => + right(await GroupWithProxies.fromProxies(proxies, mode)), + ); + }, + ) + .getOrElse((l) => throw l) + .run(); + } + + ClashFacade get _clash => ref.read(clashFacadeProvider); + + Future changeProxy(String selectorName, String proxyName) async { + loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName "); + await _clash + .changeProxy(selectorName, proxyName) + .getOrElse((l) => throw l) + .run(); + ref.invalidateSelf(); + } +} diff --git a/lib/features/proxies/view/proxies_page.dart b/lib/features/proxies/view/proxies_page.dart new file mode 100644 index 00000000..6a1e1723 --- /dev/null +++ b/lib/features/proxies/view/proxies_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/features/proxies/notifier/notifier.dart'; +import 'package:hiddify/features/proxies/notifier/proxies_delay_notifier.dart'; +import 'package:hiddify/features/proxies/widgets/widgets.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +// TODO: rewrite, bugs with scroll +class ProxiesPage extends HookConsumerWidget with PresLogger { + const ProxiesPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final notifier = ref.watch(proxiesNotifierProvider.notifier); + final asyncProxies = ref.watch(proxiesNotifierProvider); + final proxies = asyncProxies.value ?? []; + final delays = ref.watch(proxiesDelayNotifierProvider); + + final selectActiveProxyMutation = useMutation( + initialOnFailure: (error) => + CustomToast.error(t.presentError(error)).show(context), + ); + + final tabController = useTabController( + initialLength: proxies.length, + keys: [proxies.length], + ); + + switch (asyncProxies) { + case AsyncData(value: final proxies): + if (proxies.isEmpty) { + return Scaffold( + body: CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.proxies.pageTitle.titleCase), + ), + SliverFillRemaining( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(t.proxies.emptyProxiesMsg.titleCase), + ], + ), + ), + ], + ), + ); + } + + final tabs = [ + for (final groupWithProxies in proxies) + Tab( + child: Text( + groupWithProxies.group.name.toUpperCase(), + style: TextStyle( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ) + ]; + + final tabViews = [ + for (final groupWithProxies in proxies) + SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) { + return CustomScrollView( + key: PageStorageKey( + groupWithProxies.group.name, + ), + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final proxy = groupWithProxies.proxies[index]; + return ProxyTile( + proxy, + selected: groupWithProxies.group.now == proxy.name, + delay: delays[proxy.name], + onSelect: () async { + if (selectActiveProxyMutation + .state.isInProgress) { + return; + } + selectActiveProxyMutation.setFuture( + notifier.changeProxy( + groupWithProxies.group.name, + proxy.name, + ), + ); + }, + ); + }, + itemCount: groupWithProxies.proxies.length, + ), + ], + ); + }, + ), + ), + ]; + + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + NestedTabAppBar( + title: Text(t.proxies.pageTitle.titleCase), + forceElevated: innerBoxIsScrolled, + actions: [ + PopupMenuButton( + itemBuilder: (_) { + return [ + PopupMenuItem( + onTap: ref + .read(proxiesDelayNotifierProvider.notifier) + .cancelDelayTest, + child: Text( + t.proxies.cancelTestButtonText.sentenceCase, + ), + ), + ]; + }, + ), + ], + bottom: TabBar( + controller: tabController, + isScrollable: true, + tabs: tabs, + ), + ), + ]; + }, + body: TabBarView( + controller: tabController, + children: tabViews, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async => + // TODO: improve + ref.read(proxiesDelayNotifierProvider.notifier).testDelay( + proxies[tabController.index].proxies.map((e) => e.name), + ), + tooltip: t.proxies.delayTestTooltip.titleCase, + child: const Icon(Icons.bolt), + ), + ); + + case AsyncError(:final error): + return Scaffold( + body: CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.proxies.pageTitle.titleCase), + ), + SliverErrorBodyPlaceholder(t.presentError(error)), + ], + ), + ); + + case AsyncLoading(): + return Scaffold( + body: CustomScrollView( + slivers: [ + NestedTabAppBar( + title: Text(t.proxies.pageTitle.titleCase), + ), + const SliverLoadingBodyPlaceholder(), + ], + ), + ); + + // TODO: remove + default: + return const Scaffold(); + } + } +} diff --git a/lib/features/proxies/view/view.dart b/lib/features/proxies/view/view.dart new file mode 100644 index 00000000..b35ebe17 --- /dev/null +++ b/lib/features/proxies/view/view.dart @@ -0,0 +1 @@ +export 'proxies_page.dart'; diff --git a/lib/features/proxies/widgets/proxy_tile.dart b/lib/features/proxies/widgets/proxy_tile.dart new file mode 100644 index 00000000..c02854a2 --- /dev/null +++ b/lib/features/proxies/widgets/proxy_tile.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +// TODO: rewrite +class ProxyTile extends HookConsumerWidget { + const ProxyTile( + this.proxy, { + super.key, + required this.selected, + required this.onSelect, + this.delay, + }); + + final ClashProxy proxy; + final bool selected; + final VoidCallback onSelect; + final int? delay; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListTile( + title: Text( + proxy.name, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(proxy.type.label), + trailing: delay != null ? Text(delay.toString()) : null, + selected: selected, + onTap: onSelect, + ); + } +} diff --git a/lib/features/proxies/widgets/widgets.dart b/lib/features/proxies/widgets/widgets.dart new file mode 100644 index 00000000..6565a8f3 --- /dev/null +++ b/lib/features/proxies/widgets/widgets.dart @@ -0,0 +1 @@ +export 'proxy_tile.dart'; diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart new file mode 100644 index 00000000..04a8baf8 --- /dev/null +++ b/lib/features/settings/view/settings_page.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/features/settings/widgets/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class SettingsPage extends HookConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + const divider = Divider(indent: 16, endIndent: 16); + + return Scaffold( + appBar: AppBar( + title: Text(t.settings.pageTitle.titleCase), + ), + body: ListTileTheme( + data: ListTileTheme.of(context).copyWith( + contentPadding: const EdgeInsetsDirectional.only(start: 48, end: 16), + ), + child: ListView( + children: [ + _SettingsSectionHeader( + t.settings.appearance.sectionTitle.titleCase, + ), + const AppearanceSettingTiles(), + divider, + _SettingsSectionHeader(t.settings.network.sectionTitle.titleCase), + const NetworkSettingTiles(), + divider, + _SettingsSectionHeader(t.settings.clash.sectionTitle.titleCase), + const ClashSettingTiles(), + const Gap(16), + ], + ), + ), + ); + } +} + +class _SettingsSectionHeader extends StatelessWidget { + const _SettingsSectionHeader(this.title); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } +} diff --git a/lib/features/settings/view/view.dart b/lib/features/settings/view/view.dart new file mode 100644 index 00000000..10ba3ace --- /dev/null +++ b/lib/features/settings/view/view.dart @@ -0,0 +1 @@ +export 'settings_page.dart'; diff --git a/lib/features/settings/widgets/appearance_setting_tiles.dart b/lib/features/settings/widgets/appearance_setting_tiles.dart new file mode 100644 index 00000000..2465c4c8 --- /dev/null +++ b/lib/features/settings/widgets/appearance_setting_tiles.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/theme/theme.dart'; +import 'package:hiddify/features/settings/widgets/theme_mode_switch_button.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class AppearanceSettingTiles extends HookConsumerWidget { + const AppearanceSettingTiles({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final theme = ref.watch(themeControllerProvider); + final themeController = ref.watch(themeControllerProvider.notifier); + + return Column( + children: [ + ListTile( + title: Text(t.settings.appearance.themeMode.titleCase), + subtitle: Text( + switch (theme.themeMode) { + ThemeMode.system => t.settings.appearance.themeModes.system, + ThemeMode.light => t.settings.appearance.themeModes.light, + ThemeMode.dark => t.settings.appearance.themeModes.dark, + } + .sentenceCase, + ), + trailing: ThemeModeSwitch( + themeMode: theme.themeMode, + onChanged: (value) { + themeController.change(themeMode: value); + }, + ), + onTap: () async { + await themeController.change( + themeMode: Theme.of(context).brightness == Brightness.light + ? ThemeMode.dark + : ThemeMode.light, + ); + }, + ), + SwitchListTile( + title: Text(t.settings.appearance.trueBlack.titleCase), + value: theme.trueBlack, + onChanged: (value) { + themeController.change(trueBlack: value); + }, + ), + ], + ); + } +} diff --git a/lib/features/settings/widgets/clash_setting_tiles.dart b/lib/features/settings/widgets/clash_setting_tiles.dart new file mode 100644 index 00000000..383edd86 --- /dev/null +++ b/lib/features/settings/widgets/clash_setting_tiles.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class ClashSettingTiles extends HookConsumerWidget { + const ClashSettingTiles({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final overrides = + ref.watch(prefsControllerProvider.select((value) => value.clash)); + final notifier = ref.watch(prefsControllerProvider.notifier); + + return Column( + children: [ + InputOverrideTile( + title: t.settings.clash.overrides.httpPort, + value: overrides.httpPort, + resetValue: ClashConfig.initial.httpPort, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(httpPort: value), + ), + ), + InputOverrideTile( + title: t.settings.clash.overrides.socksPort, + value: overrides.socksPort, + resetValue: ClashConfig.initial.socksPort, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(socksPort: value), + ), + ), + InputOverrideTile( + title: t.settings.clash.overrides.redirPort, + value: overrides.redirPort, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(redirPort: value), + ), + ), + InputOverrideTile( + title: t.settings.clash.overrides.tproxyPort, + value: overrides.tproxyPort, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(tproxyPort: value), + ), + ), + InputOverrideTile( + title: t.settings.clash.overrides.mixedPort, + value: overrides.mixedPort, + resetValue: ClashConfig.initial.mixedPort, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(mixedPort: value), + ), + ), + ToggleOverrideTile( + title: t.settings.clash.overrides.allowLan, + value: overrides.allowLan, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(allowLan: value), + ), + ), + ToggleOverrideTile( + title: t.settings.clash.overrides.ipv6, + value: overrides.ipv6, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(ipv6: value), + ), + ), + ChoiceOverrideTile( + title: t.settings.clash.overrides.mode, + value: overrides.mode, + options: TunnelMode.values, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(mode: value), + ), + ), + ChoiceOverrideTile( + title: t.settings.clash.overrides.logLevel, + value: overrides.logLevel, + options: LogLevel.values, + onChange: (value) => notifier.patchClashOverrides( + ClashConfigPatch(logLevel: value), + ), + ), + ], + ); + } +} + +class InputOverrideTile extends HookConsumerWidget { + const InputOverrideTile({ + super.key, + required this.title, + required this.value, + this.resetValue, + required this.onChange, + }); + + final String title; + final int? value; + final int? resetValue; + final ValueChanged> onChange; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return ListTile( + title: Text(title), + leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium, + trailing: Text( + value == null + ? t.settings.clash.doNotModify.sentenceCase + : value.toString(), + ), + onTap: () async { + final result = await SettingsInputDialog( + title: title, + initialValue: value, + resetValue: optionOf(resetValue), + ).show(context).then( + (value) { + return value?.match?>( + () => none(), + (t) { + final i = int.tryParse(t); + return i == null ? null : some(i); + }, + ); + }, + ); + if (result == null) return; + onChange(result); + }, + ); + } +} + +class ToggleOverrideTile extends HookConsumerWidget { + const ToggleOverrideTile({ + super.key, + required this.title, + required this.value, + required this.onChange, + }); + + final String title; + final bool? value; + final ValueChanged> onChange; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return PopupMenuButton>( + initialValue: optionOf(value), + onSelected: onChange, + child: ListTile( + title: Text(title), + leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium, + trailing: Text( + (value == null + ? t.settings.clash.doNotModify + : value! + ? t.general.toggle.enabled + : t.general.toggle.disabled) + .sentenceCase, + ), + ), + itemBuilder: (_) { + return [ + PopupMenuItem( + value: none(), + child: Text(t.settings.clash.doNotModify.sentenceCase), + ), + PopupMenuItem( + value: some(true), + child: Text(t.general.toggle.enabled.sentenceCase), + ), + PopupMenuItem( + value: some(false), + child: Text(t.general.toggle.disabled.sentenceCase), + ), + ]; + }, + ); + } +} + +class ChoiceOverrideTile extends HookConsumerWidget { + const ChoiceOverrideTile({ + super.key, + required this.title, + required this.value, + required this.options, + required this.onChange, + }); + + final String title; + final T? value; + final List options; + final ValueChanged> onChange; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return PopupMenuButton>( + initialValue: optionOf(value), + onSelected: onChange, + child: ListTile( + title: Text(title), + leadingAndTrailingTextStyle: Theme.of(context).textTheme.bodyMedium, + trailing: Text( + (value == null ? t.settings.clash.doNotModify : value!.name) + .sentenceCase, + ), + ), + itemBuilder: (_) { + return [ + PopupMenuItem( + value: none(), + child: Text(t.settings.clash.doNotModify.sentenceCase), + ), + ...options.map( + (e) => PopupMenuItem( + value: some(e), + child: Text(e.name.sentenceCase), + ), + ), + ]; + }, + ); + } +} diff --git a/lib/features/settings/widgets/network_setting_tiles.dart b/lib/features/settings/widgets/network_setting_tiles.dart new file mode 100644 index 00000000..43d870bc --- /dev/null +++ b/lib/features/settings/widgets/network_setting_tiles.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class NetworkSettingTiles extends HookConsumerWidget { + const NetworkSettingTiles({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final prefs = + ref.watch(prefsControllerProvider.select((value) => value.network)); + final notifier = ref.watch(prefsControllerProvider.notifier); + + return Column( + children: [ + SwitchListTile( + title: Text(t.settings.network.systemProxy.titleCase), + subtitle: Text(t.settings.network.systemProxyMsg), + value: prefs.systemProxy, + onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value), + ), + SwitchListTile( + title: Text(t.settings.network.bypassPrivateNetworks.titleCase), + subtitle: Text(t.settings.network.bypassPrivateNetworksMsg), + value: prefs.bypassPrivateNetworks, + onChanged: (value) => + notifier.patchNetworkPrefs(bypassPrivateNetworks: value), + ), + ], + ); + } +} diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart new file mode 100644 index 00000000..19f8664e --- /dev/null +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SettingsInputDialog extends HookConsumerWidget with PresLogger { + const SettingsInputDialog({ + super.key, + required this.title, + this.initialValue, + this.resetValue = const None(), + this.icon, + }); + + final String title; + final T? initialValue; + + /// default value, useful for mandatory fields + final Option resetValue; + final IconData? icon; + + Future?> show(BuildContext context) async { + return showDialog( + context: context, + useRootNavigator: true, + builder: (context) => this, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final localizations = MaterialLocalizations.of(context); + + final textController = useTextEditingController( + text: initialValue?.toString(), + ); + + return AlertDialog( + title: Text(title), + icon: icon != null ? Icon(icon) : null, + content: TextFormField( + controller: textController, + inputFormatters: [ + FilteringTextInputFormatter.singleLineFormatter, + ], + autovalidateMode: AutovalidateMode.always, + ), + actions: [ + TextButton( + onPressed: () async { + await Navigator.of(context) + .maybePop(resetValue.map((t) => t.toString())); + }, + child: Text(t.general.reset.toUpperCase()), + ), + TextButton( + onPressed: () async { + await Navigator.of(context).maybePop(); + }, + child: Text(localizations.cancelButtonLabel.toUpperCase()), + ), + TextButton( + onPressed: () async { + // onConfirm(textController.value.text); + await Navigator.of(context) + .maybePop(some(textController.value.text)); + }, + child: Text(localizations.okButtonLabel.toUpperCase()), + ), + ], + ); + } +} diff --git a/lib/features/settings/widgets/theme_mode_switch_button.dart b/lib/features/settings/widgets/theme_mode_switch_button.dart new file mode 100644 index 00000000..164d85da --- /dev/null +++ b/lib/features/settings/widgets/theme_mode_switch_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ThemeModeSwitch extends StatelessWidget { + const ThemeModeSwitch({ + super.key, + required this.themeMode, + required this.onChanged, + }); + final ThemeMode themeMode; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final List isSelected = [ + themeMode == ThemeMode.light, + themeMode == ThemeMode.system, + themeMode == ThemeMode.dark, + ]; + + return ToggleButtons( + isSelected: isSelected, + onPressed: (int newIndex) { + if (newIndex == 0) { + onChanged(ThemeMode.light); + } else if (newIndex == 1) { + onChanged(ThemeMode.system); + } else { + onChanged(ThemeMode.dark); + } + }, + children: const [ + Icon(Icons.wb_sunny), + Icon(Icons.phone_iphone), + Icon(Icons.bedtime), + ], + ); + } +} diff --git a/lib/features/settings/widgets/widgets.dart b/lib/features/settings/widgets/widgets.dart new file mode 100644 index 00000000..eab6f4d1 --- /dev/null +++ b/lib/features/settings/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'appearance_setting_tiles.dart'; +export 'clash_setting_tiles.dart'; +export 'network_setting_tiles.dart'; diff --git a/lib/features/system_tray/controller/system_tray_controller.dart b/lib/features/system_tray/controller/system_tray_controller.dart new file mode 100644 index 00000000..16704ec2 --- /dev/null +++ b/lib/features/system_tray/controller/system_tray_controller.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/features/common/clash/clash_mode.dart'; +import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +part 'system_tray_controller.g.dart'; + +// TODO: rewrite +@Riverpod(keepAlive: true) +class SystemTrayController extends _$SystemTrayController + with TrayListener, AppLogger { + @override + Future build() async { + await trayManager.setIcon(Assets.images.logoRound); + trayManager.addListener(this); + ref.onDispose(() { + loggy.debug('disposing'); + trayManager.removeListener(this); + }); + ref.listen( + connectivityControllerProvider, + (_, next) async { + connection = next; + await _updateTray(); + }, + fireImmediately: true, + ); + ref.listen( + clashModeProvider.select((value) => value.valueOrNull), + (_, next) async { + mode = next; + await _updateTray(); + }, + fireImmediately: true, + ); + } + + late ConnectionStatus connection; + late TunnelMode? mode; + + Future _updateTray() async { + final t = ref.watch(translationsProvider); + final isVisible = await windowManager.isVisible(); + final trayMenu = Menu( + items: [ + MenuItem.checkbox( + label: t.tray.dashboard, + checked: isVisible, + onClick: handleClickShowApp, + ), + if (mode != null) ...[ + MenuItem.separator(), + ...TunnelMode.values.map( + (e) => MenuItem.checkbox( + label: e.name, + checked: e == mode, + onClick: (mi) => handleClickModeItem(e, mi), + ), + ), + ], + MenuItem.separator(), + MenuItem.checkbox( + label: t.tray.systemProxy, + checked: connection.isConnected, + disabled: connection.isSwitching, + onClick: handleClickSetAsSystemProxy, + ), + MenuItem.separator(), + MenuItem( + label: t.tray.quit, + onClick: handleClickExitApp, + ), + ], + ); + await trayManager.setContextMenu(trayMenu); + } + + @override + Future onTrayIconRightMouseDown() async { + super.onTrayIconRightMouseDown(); + await trayManager.popUpContextMenu(); + } + + Future handleClickShowApp(MenuItem menuItem) async { + if (menuItem.checked == true) { + await windowManager.close(); + } else { + await windowManager.show(); + } + } + + Future handleClickModeItem( + TunnelMode mode, + MenuItem menuItem, + ) async { + return ref + .read(prefsControllerProvider.notifier) + .patchClashOverrides(ClashConfigPatch(mode: some(mode))); + } + + Future handleClickSetAsSystemProxy(MenuItem menuItem) async { + return ref.read(connectivityControllerProvider.notifier).toggleConnection(); + } + + Future handleClickExitApp(MenuItem menuItem) async { + exit(0); + } +} diff --git a/lib/features/system_tray/system_tray.dart b/lib/features/system_tray/system_tray.dart new file mode 100644 index 00000000..4d5727f7 --- /dev/null +++ b/lib/features/system_tray/system_tray.dart @@ -0,0 +1 @@ +export 'controller/system_tray_controller.dart'; diff --git a/lib/features/wrapper/view/desktop_wrapper.dart b/lib/features/wrapper/view/desktop_wrapper.dart new file mode 100644 index 00000000..04910dac --- /dev/null +++ b/lib/features/wrapper/view/desktop_wrapper.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/common/traffic/traffic_chart.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class DesktopWrapper extends HookConsumerWidget { + const DesktopWrapper(this.navigator, {super.key}); + + final Widget navigator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final currentIndex = getCurrentIndex(context); + + final destinations = [ + NavigationRailDestination( + icon: const Icon(Icons.power_settings_new), + label: Text(t.home.pageTitle.titleCase), + ), + NavigationRailDestination( + icon: const Icon(Icons.filter_list), + label: Text(t.proxies.pageTitle.titleCase), + ), + NavigationRailDestination( + icon: const Icon(Icons.article), + label: Text(t.logs.pageTitle.titleCase), + ), + NavigationRailDestination( + icon: const Icon(Icons.settings), + label: Text(t.settings.pageTitle.titleCase), + ), + ]; + + return Scaffold( + body: Row( + children: [ + SizedBox( + width: 192, + child: NavigationRail( + extended: true, + minExtendedWidth: 192, + destinations: destinations, + selectedIndex: currentIndex, + onDestinationSelected: (index) => switchTab(index, context), + trailing: const Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: TrafficChart(), + ), + ), + ), + ), + Expanded(child: navigator), + ], + ), + ); + } +} diff --git a/lib/features/wrapper/view/mobile_wrapper.dart b/lib/features/wrapper/view/mobile_wrapper.dart new file mode 100644 index 00000000..ebd15fab --- /dev/null +++ b/lib/features/wrapper/view/mobile_wrapper.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/common/common.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:recase/recase.dart'; + +class MobileWrapper extends HookConsumerWidget { + const MobileWrapper(this.navigator, {super.key}); + + final Widget navigator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final currentIndex = getCurrentIndex(context); + final location = GoRouterState.of(context).location; + + return Scaffold( + key: RootScaffold.stateKey, + body: navigator, + drawer: SafeArea( + child: Drawer( + width: (MediaQuery.of(context).size.width * 0.88).clamp(0, 304), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Gap(16), + DrawerTile( + label: t.settings.pageTitle.titleCase, + icon: Icons.settings, + selected: location == SettingsRoute.path, + onSelect: () => const SettingsRoute().push(context), + ), + DrawerTile( + label: t.logs.pageTitle.titleCase, + icon: Icons.article, + selected: location == LogsRoute.path, + onSelect: () => const LogsRoute().push(context), + ), + const Spacer(), + Align( + child: Column( + children: [ + Assets.images.logo.svg(width: 64), + const Gap(8), + Text( + t.general.appTitle.titleCase, + style: Theme.of(context).textTheme.titleSmall, + ), + ], + ), + ), + const Gap(16), + ], + ), + ), + ), + bottomNavigationBar: NavigationBar( + destinations: [ + NavigationDestination( + icon: const Icon(Icons.power_settings_new), + label: t.home.pageTitle.titleCase, + ), + NavigationDestination( + icon: const Icon(Icons.filter_list), + label: t.proxies.pageTitle.titleCase, + ), + ], + selectedIndex: currentIndex > 1 ? 0 : currentIndex, + onDestinationSelected: (index) => switchTab(index, context), + ), + ); + } +} + +class DrawerTile extends StatelessWidget { + const DrawerTile({ + super.key, + required this.label, + required this.icon, + required this.selected, + required this.onSelect, + }); + + final String label; + final IconData icon; + final bool selected; + final VoidCallback onSelect; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(label), + leading: Icon(icon), + selected: selected, + onTap: selected ? () {} : onSelect, + ); + } +} diff --git a/lib/features/wrapper/wrapper.dart b/lib/features/wrapper/wrapper.dart new file mode 100644 index 00000000..867e049a --- /dev/null +++ b/lib/features/wrapper/wrapper.dart @@ -0,0 +1,2 @@ +export 'view/desktop_wrapper.dart'; +export 'view/mobile_wrapper.dart'; diff --git a/lib/gen/clash_generated_bindings.dart b/lib/gen/clash_generated_bindings.dart new file mode 100644 index 00000000..f39213fa --- /dev/null +++ b/lib/gen/clash_generated_bindings.dart @@ -0,0 +1,1311 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +/// Bindings to Clash +class ClashNativeLibrary { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + ClashNativeLibrary(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + ClashNativeLibrary.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)>(); + + void getConfigs( + int port, + ) { + return _getConfigs( + port, + ); + } + + late final _getConfigsPtr = + _lookup>( + 'getConfigs'); + late final _getConfigs = _getConfigsPtr.asFunction(); + + void patchConfigs( + int port, + ffi.Pointer patchStr, + ) { + return _patchConfigs( + port, + patchStr, + ); + } + + late final _patchConfigsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.LongLong, ffi.Pointer)>>('patchConfigs'); + late final _patchConfigs = + _patchConfigsPtr.asFunction)>(); + + void updateConfigs( + int port, + ffi.Pointer pathStr, + int force, + ) { + return _updateConfigs( + port, + pathStr, + force, + ); + } + + late final _updateConfigsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.LongLong, ffi.Pointer, GoUint8)>>('updateConfigs'); + late final _updateConfigs = _updateConfigsPtr + .asFunction, int)>(); + + void validateConfig( + int port, + ffi.Pointer path, + ) { + return _validateConfig( + port, + path, + ); + } + + late final _validateConfigPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.LongLong, ffi.Pointer)>>('validateConfig'); + late final _validateConfig = _validateConfigPtr + .asFunction)>(); + + void initNativeDartBridge( + ffi.Pointer api, + ) { + return _initNativeDartBridge( + api, + ); + } + + late final _initNativeDartBridgePtr = + _lookup)>>( + 'initNativeDartBridge'); + late final _initNativeDartBridge = _initNativeDartBridgePtr + .asFunction)>(); + + void setOptions( + int port, + ffi.Pointer dir, + ffi.Pointer configPath, + ) { + return _setOptions( + port, + dir, + configPath, + ); + } + + late final _setOptionsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.LongLong, ffi.Pointer, + ffi.Pointer)>>('setOptions'); + late final _setOptions = _setOptionsPtr.asFunction< + void Function(int, ffi.Pointer, ffi.Pointer)>(); + + void start( + int port, + ) { + return _start( + port, + ); + } + + late final _startPtr = + _lookup>('start'); + late final _start = _startPtr.asFunction(); + + void startLog( + int port, + ffi.Pointer levelStr, + ) { + return _startLog( + port, + levelStr, + ); + } + + late final _startLogPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.LongLong, ffi.Pointer)>>('startLog'); + late final _startLog = + _startLogPtr.asFunction)>(); + + void stopLog() { + return _stopLog(); + } + + late final _stopLogPtr = + _lookup>('stopLog'); + late final _stopLog = _stopLogPtr.asFunction(); + + void getProxies( + int port, + ) { + return _getProxies( + port, + ); + } + + late final _getProxiesPtr = + _lookup>( + 'getProxies'); + late final _getProxies = _getProxiesPtr.asFunction(); + + void updateProxy( + int port, + ffi.Pointer selectorName, + ffi.Pointer proxyName, + ) { + return _updateProxy( + port, + selectorName, + proxyName, + ); + } + + late final _updateProxyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.LongLong, ffi.Pointer, + ffi.Pointer)>>('updateProxy'); + late final _updateProxy = _updateProxyPtr.asFunction< + void Function(int, ffi.Pointer, ffi.Pointer)>(); + + void getProxyDelay( + int port, + ffi.Pointer name, + ffi.Pointer url, + int timeout, + ) { + return _getProxyDelay( + port, + name, + url, + timeout, + ); + } + + late final _getProxyDelayPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.LongLong, ffi.Pointer, + ffi.Pointer, ffi.Long)>>('getProxyDelay'); + late final _getProxyDelay = _getProxyDelayPtr.asFunction< + void Function(int, ffi.Pointer, ffi.Pointer, int)>(); + + void getTraffic( + int port, + ) { + return _getTraffic( + port, + ); + } + + late final _getTrafficPtr = + _lookup>( + 'getTraffic'); + late final _getTraffic = _getTrafficPtr.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; + +final class _GoString_ extends ffi.Struct { + external ffi.Pointer p; + + @ptrdiff_t() + external int n; +} + +typedef ptrdiff_t = ffi.LongLong; + +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 GoUint8 = ffi.UnsignedChar; + +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; + +const int INT8_MIN = -128; + +const int INT16_MIN = -32768; + +const int INT32_MIN = -2147483648; + +const int INT64_MIN = -9223372036854775808; + +const int INT8_MAX = 127; + +const int INT16_MAX = 32767; + +const int INT32_MAX = 2147483647; + +const int INT64_MAX = 9223372036854775807; + +const int UINT8_MAX = 255; + +const int UINT16_MAX = 65535; + +const int UINT32_MAX = 4294967295; + +const int UINT64_MAX = -1; + +const int INT_LEAST8_MIN = -128; + +const int INT_LEAST16_MIN = -32768; + +const int INT_LEAST32_MIN = -2147483648; + +const int INT_LEAST64_MIN = -9223372036854775808; + +const int INT_LEAST8_MAX = 127; + +const int INT_LEAST16_MAX = 32767; + +const int INT_LEAST32_MAX = 2147483647; + +const int INT_LEAST64_MAX = 9223372036854775807; + +const int UINT_LEAST8_MAX = 255; + +const int UINT_LEAST16_MAX = 65535; + +const int UINT_LEAST32_MAX = 4294967295; + +const int UINT_LEAST64_MAX = -1; + +const int INT_FAST8_MIN = -128; + +const int INT_FAST16_MIN = -2147483648; + +const int INT_FAST32_MIN = -2147483648; + +const int INT_FAST64_MIN = -9223372036854775808; + +const int INT_FAST8_MAX = 127; + +const int INT_FAST16_MAX = 2147483647; + +const int INT_FAST32_MAX = 2147483647; + +const int INT_FAST64_MAX = 9223372036854775807; + +const int UINT_FAST8_MAX = 255; + +const int UINT_FAST16_MAX = 4294967295; + +const int UINT_FAST32_MAX = 4294967295; + +const int UINT_FAST64_MAX = -1; + +const int INTPTR_MIN = -9223372036854775808; + +const int INTPTR_MAX = 9223372036854775807; + +const int UINTPTR_MAX = -1; + +const int INTMAX_MIN = -9223372036854775808; + +const int INTMAX_MAX = 9223372036854775807; + +const int UINTMAX_MAX = -1; + +const int PTRDIFF_MIN = -9223372036854775808; + +const int PTRDIFF_MAX = 9223372036854775807; + +const int SIZE_MAX = -1; + +const int SIG_ATOMIC_MIN = -2147483648; + +const int SIG_ATOMIC_MAX = 2147483647; + +const int WCHAR_MIN = 0; + +const int WCHAR_MAX = 65535; + +const int WINT_MIN = 0; + +const int WINT_MAX = 65535; diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 00000000..52630a4d --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,7 @@ +import 'package:flutter/widgets.dart'; +import 'package:hiddify/bootstrap.dart'; + +void main() async { + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + return lazyBootstrap(widgetsBinding); +} diff --git a/lib/services/clash/async_ffi.dart b/lib/services/clash/async_ffi.dart new file mode 100644 index 00000000..aa8da0dd --- /dev/null +++ b/lib/services/clash/async_ffi.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:hiddify/services/clash/async_ffi_response.dart'; +import 'package:hiddify/utils/utils.dart'; + +// TODO: add timeout +// TODO: test and improve +mixin AsyncFFI implements LoggerMixin { + Future runAsync(void Function(int port) run) async { + final receivePort = ReceivePort(); + final responseFuture = receivePort.map( + (event) { + if (event is String) { + receivePort.close(); + return AsyncFfiResponse.fromJson( + jsonDecode(event) as Map, + ); + } + receivePort.close(); + throw Exception("unexpected data type[${event.runtimeType}]"); + }, + ).first; + run(receivePort.sendPort.nativePort); + return responseFuture; + } +} diff --git a/lib/services/clash/async_ffi_response.dart b/lib/services/clash/async_ffi_response.dart new file mode 100644 index 00000000..88615d20 --- /dev/null +++ b/lib/services/clash/async_ffi_response.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'async_ffi_response.freezed.dart'; +part 'async_ffi_response.g.dart'; + +@freezed +class AsyncFfiResponse with _$AsyncFfiResponse { + const AsyncFfiResponse._(); + + const factory AsyncFfiResponse({ + @JsonKey(name: 'success') required bool success, + @JsonKey(name: 'message') String? message, + @JsonKey(name: 'data') String? data, + }) = _AsyncFfiResponse; + + factory AsyncFfiResponse.fromJson(Map json) => + _$AsyncFfiResponseFromJson(json); +} diff --git a/lib/services/clash/clash.dart b/lib/services/clash/clash.dart new file mode 100644 index 00000000..6ee6196d --- /dev/null +++ b/lib/services/clash/clash.dart @@ -0,0 +1,2 @@ +export 'clash_service.dart'; +export 'clash_service_impl.dart'; diff --git a/lib/services/clash/clash_service.dart b/lib/services/clash/clash_service.dart new file mode 100644 index 00000000..db4ecf14 --- /dev/null +++ b/lib/services/clash/clash_service.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/clash/clash.dart'; + +abstract class ClashService { + Future init(); + + Future start({String configFileName = "config"}); + + TaskEither validateConfig(String configPath); + + TaskEither> getProxies(); + + TaskEither changeProxy( + String selectorName, + String proxyName, + ); + + TaskEither getProxyDelay( + String name, + String url, { + Duration timeout = const Duration(seconds: 10), + }); + + TaskEither getConfigs(); + + TaskEither updateConfigs(String path); + + TaskEither patchConfigs(ClashConfig config); + + Stream watchLogs(LogLevel level); + + TaskEither getTraffic(); +} diff --git a/lib/services/clash/clash_service_impl.dart b/lib/services/clash/clash_service_impl.dart new file mode 100644 index 00000000..50c16223 --- /dev/null +++ b/lib/services/clash/clash_service_impl.dart @@ -0,0 +1,235 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:combine/combine.dart'; +import 'package:ffi/ffi.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/clash/clash.dart'; +import 'package:hiddify/gen/clash_generated_bindings.dart'; +import 'package:hiddify/services/clash/async_ffi.dart'; +import 'package:hiddify/services/clash/clash_service.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:path/path.dart' as p; +import 'package:rxdart/rxdart.dart'; + +// TODO: logging has potential memory leak +class ClashServiceImpl with AsyncFFI, InfraLogger implements ClashService { + ClashServiceImpl({required this.filesEditor}); + + final FilesEditorService filesEditor; + + late final ClashNativeLibrary _clash; + + @override + Future init() async { + loggy.debug('initializing'); + _initClashLib(); + _clash.initNativeDartBridge(NativeApi.initializeApiDLData); + } + + void _initClashLib() { + String fullPath = ""; + if (Platform.environment.containsKey('FLUTTER_TEST')) { + fullPath = "core"; + } + if (Platform.isWindows) { + fullPath = p.join(fullPath, "libclash.dll"); + } else if (Platform.isMacOS) { + fullPath = p.join(fullPath, "libclash.dylib"); + } else { + fullPath = p.join(fullPath, "libclash.so"); + } + loggy.debug('clash native libs path: "$fullPath"'); + final lib = DynamicLibrary.open(fullPath); + _clash = ClashNativeLibrary(lib); + } + + @override + Future start({String configFileName = "config"}) async { + loggy.debug('starting clash with config: [$configFileName]'); + final stopWatch = Stopwatch()..start(); + final configPath = filesEditor.configPath(configFileName); + final response = await runAsync( + (port) => _clash.setOptions( + port, + filesEditor.clashDirPath.toNativeUtf8().cast(), + configPath.toNativeUtf8().cast(), + ), + ); + if (!response.success) throw ClashFailure.core(response.message); + stopWatch.stop(); + loggy.info( + "started clash service [${stopWatch.elapsedMilliseconds}ms]", + ); + } + + @override + TaskEither validateConfig(String configPath) { + return TaskEither( + () async { + final response = await runAsync( + (port) => + _clash.validateConfig(port, configPath.toNativeUtf8().cast()), + ); + if (!response.success) return left(response.message ?? ''); + return right(response.data! == "true"); + }, + ); + } + + @override + TaskEither updateConfigs(String path) { + return TaskEither(() async { + final stopWatch = Stopwatch()..start(); + final response = await runAsync( + (port) => _clash.updateConfigs(port, path.toNativeUtf8().cast(), 0), + ); + stopWatch.stop(); + if (response.success) { + loggy.info("changed config in [${stopWatch.elapsedMilliseconds}ms]"); + return right(unit); + } + return left(response.message ?? ''); + }); + } + + @override + TaskEither> getProxies() { + return TaskEither( + () async { + final response = await runAsync((port) => _clash.getProxies(port)); + if (!response.success) return left(response.message ?? ""); + final proxies = await CombineWorker().executeWithArg( + (data) { + if (data == null) return []; + final json = jsonDecode(data)['proxies'] as Map; + final parsed = json.entries.map( + (e) { + final proxyMap = (e.value as Map) + ..putIfAbsent('name', () => e.key); + return ClashProxy.fromJson(proxyMap); + }, + ).toList(); + return parsed; + }, + response.data, + ); + return right(proxies); + }, + ); + } + + @override + TaskEither patchConfigs(ClashConfig config) { + return TaskEither( + () async { + final response = await runAsync( + (port) => _clash.patchConfigs( + port, + jsonEncode(config.toJson()).toNativeUtf8().cast(), + ), + ); + if (!response.success) return left(response.message ?? ""); + return right(unit); + }, + ); + } + + @override + TaskEither getConfigs() { + return TaskEither( + () async { + final response = await runAsync( + (port) => _clash.getConfigs(port), + ); + if (!response.success) return left(response.message ?? ""); + return right( + ClashConfig.fromJson( + jsonDecode(response.data!) as Map, + ), + ); + }, + ); + } + + @override + TaskEither changeProxy( + String selectorName, + String proxyName, + ) { + return TaskEither( + () async { + final response = await runAsync( + (port) => _clash.updateProxy( + port, + selectorName.toNativeUtf8().cast(), + proxyName.toNativeUtf8().cast(), + ), + ); + if (!response.success) return left(response.message ?? ""); + return right(unit); + }, + ); + } + + @override + TaskEither getProxyDelay( + String name, + String url, { + Duration timeout = const Duration(seconds: 10), + }) { + return TaskEither( + () async { + final response = await runAsync( + (port) => _clash.getProxyDelay( + port, + name.toNativeUtf8().cast(), + url.toNativeUtf8().cast(), + timeout.inMilliseconds, + ), + ); + if (!response.success) return left(response.message ?? ""); + return right( + (jsonDecode(response.data!) as Map)["delay"] as int, + ); + }, + ); + } + + @override + Stream watchLogs(LogLevel level) { + final logsPort = ReceivePort(); + final logsStream = logsPort.map( + (event) { + final json = jsonDecode(event as String) as Map; + return ClashLog.fromJson(json); + }, + ); + _clash.startLog( + logsPort.sendPort.nativePort, + level.name.toNativeUtf8().cast(), + ); + return logsStream.doOnCancel(() => _clash.stopLog()); + } + + @override + TaskEither getTraffic() { + return TaskEither( + () async { + final response = await runAsync( + (port) => _clash.getTraffic(port), + ); + if (!response.success) return left(response.message ?? ""); + return right( + ClashTraffic.fromJson( + jsonDecode(response.data!) as Map, + ), + ); + }, + ); + } +} diff --git a/lib/services/connectivity/connectivity.dart b/lib/services/connectivity/connectivity.dart new file mode 100644 index 00000000..9b6161d6 --- /dev/null +++ b/lib/services/connectivity/connectivity.dart @@ -0,0 +1,3 @@ +export 'connectivity_service.dart'; +export 'desktop_connectivity_service.dart'; +export 'mobile_connectivity_service.dart'; diff --git a/lib/services/connectivity/connectivity_service.dart b/lib/services/connectivity/connectivity_service.dart new file mode 100644 index 00000000..2e58d7f7 --- /dev/null +++ b/lib/services/connectivity/connectivity_service.dart @@ -0,0 +1,27 @@ +import 'package:hiddify/services/connectivity/desktop_connectivity_service.dart'; +import 'package:hiddify/services/connectivity/mobile_connectivity_service.dart'; +import 'package:hiddify/services/notification/notification.dart'; +import 'package:hiddify/utils/utils.dart'; + +abstract class ConnectivityService { + factory ConnectivityService(NotificationService notification) { + if (PlatformUtils.isDesktop) return DesktopConnectivityService(); + return MobileConnectivityService(notification); + } + + Future init(); + + // TODO: use declarative states + Stream watchConnectionStatus(); + + // TODO: remove + Future grantVpnPermission(); + + Future connect({ + required int httpPort, + required int socksPort, + bool systemProxy = true, + }); + + Future disconnect(); +} diff --git a/lib/services/connectivity/desktop_connectivity_service.dart b/lib/services/connectivity/desktop_connectivity_service.dart new file mode 100644 index 00000000..c2b534bc --- /dev/null +++ b/lib/services/connectivity/desktop_connectivity_service.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/services/connectivity/connectivity_service.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:proxy_manager/proxy_manager.dart'; +import 'package:rxdart/rxdart.dart'; + +// TODO: rewrite +class DesktopConnectivityService + with InfraLogger + implements ConnectivityService { + // TODO: possibly replace + final _proxyManager = ProxyManager(); + + final _connectionStatus = BehaviorSubject.seeded(false); + + @override + Future init() async {} + + @override + Stream watchConnectionStatus() { + return _connectionStatus; + } + + @override + Future grantVpnPermission() async => true; + + @override + Future connect({ + required int httpPort, + required int socksPort, + bool systemProxy = true, + }) async { + loggy.debug('connecting'); + await Future.wait([ + _proxyManager.setAsSystemProxy( + ProxyTypes.http, + Constants.localHost, + httpPort, + ), + _proxyManager.setAsSystemProxy( + ProxyTypes.https, + Constants.localHost, + httpPort, + ) + ]); + if (!Platform.isWindows) { + await _proxyManager.setAsSystemProxy( + ProxyTypes.socks, + Constants.localHost, + socksPort, + ); + } + _connectionStatus.value = true; + } + + @override + Future disconnect() async { + loggy.debug("disconnecting"); + await _proxyManager.cleanSystemProxy(); + _connectionStatus.value = false; + } +} diff --git a/lib/services/connectivity/mobile_connectivity_service.dart b/lib/services/connectivity/mobile_connectivity_service.dart new file mode 100644 index 00000000..54794e8c --- /dev/null +++ b/lib/services/connectivity/mobile_connectivity_service.dart @@ -0,0 +1,86 @@ +import 'package:flutter/services.dart'; +import 'package:hiddify/domain/connectivity/connectivity_failure.dart'; +import 'package:hiddify/services/connectivity/connectivity_service.dart'; +import 'package:hiddify/services/notification/notification.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:rxdart/rxdart.dart'; + +// TODO: rewrite +class MobileConnectivityService + with InfraLogger + implements ConnectivityService { + MobileConnectivityService(this._notificationService); + + final NotificationService _notificationService; + + static const _methodChannel = MethodChannel("Hiddify/VpnService"); + static const _eventChannel = EventChannel("Hiddify/VpnServiceEvents"); + + final _connectionStatus = ValueConnectableStream( + _eventChannel.receiveBroadcastStream().map((event) => event as bool), + ).autoConnect(); + + @override + Future init() async { + loggy.debug("initializing"); + final initialStatus = _connectionStatus.first; + await _methodChannel.invokeMethod("refresh_status"); + await initialStatus; + } + + @override + Stream watchConnectionStatus() { + return _connectionStatus; + } + + @override + Future grantVpnPermission() async { + loggy.debug('requesting vpn permission'); + final result = await _methodChannel.invokeMethod("grant_permission"); + if (!(result ?? false)) { + loggy.info("vpn permission denied"); + } + return result ?? false; + } + + @override + Future connect({ + required int httpPort, + required int socksPort, + bool systemProxy = true, + }) async { + loggy.debug("connecting"); + await setPrefs(httpPort, socksPort, systemProxy); + final hasNotificationPermission = + await _notificationService.grantPermission(); + if (!hasNotificationPermission) { + loggy.warning("notification permission denied"); + throw const ConnectivityFailure.unexpected(); + } + await _methodChannel.invokeMethod("start"); + } + + @override + Future disconnect() async { + loggy.debug("disconnecting"); + await _methodChannel.invokeMethod("stop"); + } + + Future setPrefs(int port, int socksPort, bool systemProxy) async { + loggy.debug( + 'setting connection prefs: httpPort: $port, socksPort: $socksPort, systemProxy: $systemProxy', + ); + final result = await _methodChannel.invokeMethod( + "set_prefs", + { + "port": port, + "socks-port": socksPort, + "system-proxy": systemProxy, + }, + ); + if (!(result ?? false)) { + loggy.error("failed to set connection prefs"); + // TODO: throw + } + } +} diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart new file mode 100644 index 00000000..80d076db --- /dev/null +++ b/lib/services/deep_link_service.dart @@ -0,0 +1,44 @@ +import 'package:hiddify/utils/utils.dart'; +import 'package:protocol_handler/protocol_handler.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'deep_link_service.g.dart'; + +typedef NewProfileLink = ({String? url, String? name}); + +@Riverpod(keepAlive: true) +class DeepLinkService extends _$DeepLinkService + with ProtocolListener, InfraLogger { + @override + Future build() async { + for (final protocol in _protocols) { + await protocolHandler.register(protocol); + } + protocolHandler.addListener(this); + ref.onDispose(() { + protocolHandler.removeListener(this); + }); + + final initialPayload = await protocolHandler.getInitialUrl(); + if (initialPayload != null) { + loggy.debug('initial payload: [$initialPayload]'); + final link = LinkParser.deep(initialPayload); + return link; + } + return null; + } + + static const _protocols = ['clash', 'clashmeta']; + + @override + void onProtocolUrlReceived(String url) { + super.onProtocolUrlReceived(url); + loggy.debug("url received: [$url]"); + final link = LinkParser.deep(url); + if (link == null) { + loggy.debug("link was not valid"); + return; + } + update((_) => link); + } +} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart new file mode 100644 index 00000000..99fbb396 --- /dev/null +++ b/lib/services/files_editor_service.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class FilesEditorService with InfraLogger { + late final Directory _supportDir; + late final Directory _clashDirectory; + late final Directory _logsDirectory; + + Future init() async { + loggy.debug('initializing'); + _supportDir = await getApplicationSupportDirectory(); + _clashDirectory = + Directory(p.join(_supportDir.path, Constants.clashFolderName)); + loggy.debug('clash directory: $_clashDirectory'); + if (!await _clashDirectory.exists()) { + await _clashDirectory.create(recursive: true); + } + if (!await File(countryMMDBPath).exists()) { + await _populateDefaultCountryMMDB(); + } + if (!await File(defaultConfigPath).exists()) await _populateDefaultConfig(); + } + + String get clashDirPath => _clashDirectory.path; + + late final logsPath = p.join( + _logsDirectory.path, + "${DateTime.now().toUtc().toIso8601String().split('T').first}.txt", + ); + + String get defaultConfigPath => configPath("config"); + + String configPath(String fileName) { + return p.join(_clashDirectory.path, "$fileName.yaml"); + } + + Future deleteConfig(String fileName) { + return File(configPath(fileName)).delete(); + } + + String get countryMMDBPath { + return p.join( + _clashDirectory.path, + "${Constants.countryMMDBFileName}.mmdb", + ); + } + + Future _populateDefaultConfig() async { + loggy.debug('populating default config file'); + final defaultConfig = await rootBundle.load(Assets.core.clash.config); + await File(defaultConfigPath) + .writeAsBytes(defaultConfig.buffer.asInt8List()); + } + + Future _populateDefaultCountryMMDB() async { + loggy.debug('populating default country mmdb file'); + final defaultCountryMMDB = await rootBundle.load(Assets.core.clash.country); + await File(countryMMDBPath) + .writeAsBytes(defaultCountryMMDB.buffer.asInt8List()); + } +} diff --git a/lib/services/notification/constants.dart b/lib/services/notification/constants.dart new file mode 100644 index 00000000..bddbccbc --- /dev/null +++ b/lib/services/notification/constants.dart @@ -0,0 +1,9 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +const mainChannel = AndroidNotificationChannel( + "com.hiddify.hiddify", + "Hiddify", + importance: Importance.high, + enableVibration: false, + playSound: false, +); diff --git a/lib/services/notification/local_notification_service.dart b/lib/services/notification/local_notification_service.dart new file mode 100644 index 00000000..10c5f54a --- /dev/null +++ b/lib/services/notification/local_notification_service.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hiddify/services/notification/constants.dart'; +import 'package:hiddify/services/notification/notification_service.dart'; +import 'package:hiddify/utils/utils.dart'; + +// TODO: rewrite + +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + // TODO: handle action +} + +// ignore: unreachable_from_main +class LocalNotificationService with InfraLogger implements NotificationService { + LocalNotificationService(this.flutterLocalNotificationsPlugin); + + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + String? payload; + + @override + Future init() async { + loggy.debug('initializing'); + const initializationSettings = InitializationSettings( + android: AndroidInitializationSettings('mipmap/ic_launcher'), + ); + + await _initDetails(); + await _initChannels(); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + } + + Future _initDetails() async { + if (kIsWeb || Platform.isLinux) return; + final initialDetails = + await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + + if (initialDetails?.didNotificationLaunchApp ?? false) { + payload = initialDetails!.notificationResponse?.payload; + loggy.debug('app launched from notification, payload: $payload'); + // TODO: use payload + } + } + + Future _initChannels() async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(mainChannel); + } + + @override + void onDidReceiveNotificationResponse( + NotificationResponse notificationResponse, + ) { + // TODO: complete + loggy.debug('received notification response, $notificationResponse'); + } + + @override + Future showNotification({ + required int id, + required String title, + String? body, + NotificationDetails? details, + String? payload, + }) async { + loggy.debug('showing notification'); + await flutterLocalNotificationsPlugin.show( + id, + title, + body, + details ?? + NotificationDetails( + android: AndroidNotificationDetails( + mainChannel.id, + mainChannel.name, + ), + ), + payload: payload, + ); + } + + @override + Future removeNotification(int id) async { + loggy.debug('removing notification'); + await flutterLocalNotificationsPlugin.cancel(id); + } + + @override + Future grantPermission() async { + final result = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestPermission(); + return result ?? false; + } +} diff --git a/lib/services/notification/notification.dart b/lib/services/notification/notification.dart new file mode 100644 index 00000000..7dcf27d0 --- /dev/null +++ b/lib/services/notification/notification.dart @@ -0,0 +1 @@ +export 'notification_service.dart'; diff --git a/lib/services/notification/notification_service.dart b/lib/services/notification/notification_service.dart new file mode 100644 index 00000000..dfc76ce2 --- /dev/null +++ b/lib/services/notification/notification_service.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hiddify/services/notification/local_notification_service.dart'; +import 'package:hiddify/services/notification/stub_notification_service.dart'; + +abstract class NotificationService { + factory NotificationService() { + if (Platform.isWindows) return StubNotificationService(); + return LocalNotificationService(FlutterLocalNotificationsPlugin()); + } + + Future init(); + + void onDidReceiveNotificationResponse( + NotificationResponse notificationResponse, + ); + + Future grantPermission(); + + Future showNotification({ + required int id, + required String title, + String? body, + NotificationDetails? details, + String? payload, + }); + + Future removeNotification(int id); +} diff --git a/lib/services/notification/stub_notification_service.dart b/lib/services/notification/stub_notification_service.dart new file mode 100644 index 00000000..ab445c29 --- /dev/null +++ b/lib/services/notification/stub_notification_service.dart @@ -0,0 +1,31 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hiddify/services/notification/notification_service.dart'; + +class StubNotificationService implements NotificationService { + @override + Future init() async { + return; + } + + @override + void onDidReceiveNotificationResponse( + NotificationResponse notificationResponse, + ) {} + + @override + Future removeNotification(int id) async {} + + @override + Future showNotification({ + required int id, + required String title, + String? body, + NotificationDetails? details, + String? payload, + }) async {} + + @override + Future grantPermission() async { + return true; + } +} diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart new file mode 100644 index 00000000..afad7468 --- /dev/null +++ b/lib/services/service_providers.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/services/clash/clash.dart'; +import 'package:hiddify/services/connectivity/connectivity.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/services/notification/notification.dart'; +import 'package:hiddify/services/window_manager_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'service_providers.g.dart'; + +@Riverpod(keepAlive: true) +NotificationService notificationService(NotificationServiceRef ref) => + NotificationService(); + +@Riverpod(keepAlive: true) +FilesEditorService filesEditorService(FilesEditorServiceRef ref) => + FilesEditorService(); + +@Riverpod(keepAlive: true) +WindowManagerService windowManagerService(WindowManagerServiceRef ref) => + WindowManagerService(); + +@Riverpod(keepAlive: true) +ConnectivityService connectivityService(ConnectivityServiceRef ref) => + ConnectivityService( + ref.watch(notificationServiceProvider), + ); + +@Riverpod(keepAlive: true) +ClashService clashService(ClashServiceRef ref) => ClashServiceImpl( + filesEditor: ref.read(filesEditorServiceProvider), + ); diff --git a/lib/services/window_manager_service.dart b/lib/services/window_manager_service.dart new file mode 100644 index 00000000..0cf888a5 --- /dev/null +++ b/lib/services/window_manager_service.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; + +// TODO: rewrite +class WindowManagerService with WindowListener { + Future init() async { + await windowManager.ensureInitialized(); + const windowOptions = WindowOptions( + size: Size(868, 768), + minimumSize: Size(868, 648), + center: true, + ); + await windowManager.waitUntilReadyToShow(windowOptions); + windowManager.addListener(this); + } + + void dispose() { + windowManager.removeListener(this); + } +} diff --git a/lib/utils/alerts.dart b/lib/utils/alerts.dart new file mode 100644 index 00000000..905433a4 --- /dev/null +++ b/lib/utils/alerts.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +enum AlertType { info, error, success } + +class CustomToast extends StatelessWidget { + const CustomToast( + this.message, { + this.type = AlertType.info, + this.icon, + this.duration = const Duration(seconds: 3), + }); + + const CustomToast.error( + this.message, { + this.duration = const Duration(seconds: 5), + }) : type = AlertType.error, + icon = Icons.error; + + const CustomToast.success( + this.message, { + this.duration = const Duration(seconds: 3), + }) : type = AlertType.success, + icon = Icons.check; + + final String message; + final AlertType type; + final IconData? icon; + final Duration duration; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final color = switch (type) { + AlertType.info => null, + AlertType.error => scheme.error, + AlertType.success => scheme.tertiary, + }; + + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: Theme.of(context).colorScheme.surface, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon(icon, color: color), + const SizedBox(width: 8), + ], + Flexible(child: Text(message)), + ], + ), + ); + } + + void show(BuildContext context) { + FToast().init(context); + FToast().showToast( + child: this, + gravity: ToastGravity.BOTTOM, + toastDuration: duration, + ); + } +} diff --git a/lib/utils/async_mutation.dart b/lib/utils/async_mutation.dart new file mode 100644 index 00000000..4462b50a --- /dev/null +++ b/lib/utils/async_mutation.dart @@ -0,0 +1,75 @@ +// ignore_for_file: unreachable_switch_case + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'async_mutation.freezed.dart'; + +// TODO: test and improve + +@freezed +class AsyncMutation with _$AsyncMutation { + const AsyncMutation._(); + + const factory AsyncMutation.idle() = Idle; + const factory AsyncMutation.inProgress() = InProgress; + const factory AsyncMutation.fail(Object error, StackTrace stackTrace) = Fail; + const factory AsyncMutation.success() = Success; + + bool get isInProgress => this is InProgress; +} + +/// temporary(and hacky) way to manage async mutations +({ + AsyncMutation state, + ValueChanged> setFuture, + ValueChanged setOnFailure, +}) useMutation({ + void Function(Object error)? initialOnFailure, + void Function()? initialOnSuccess, +}) { + final mutationUpdate = useState?>(null); + final mutationState = useFuture(mutationUpdate.value); + final failureCallBack = + useValueNotifier(initialOnFailure); + final successCallBack = useValueNotifier(initialOnSuccess); + + // map AsyncSnapshot to AsyncMutation which is easier to consume + final mapped = useMemoized( + () => switch (mutationState) { + // ignore: unused_local_variable + AsyncSnapshot(:final data?) => const Success(), + AsyncSnapshot(connectionState: ConnectionState.waiting) => + const InProgress(), + AsyncSnapshot(:final error?, :final stackTrace?) => + Fail(error, stackTrace), + _ => const Idle(), + }, + [mutationState], + ); + + // one-of callback in failure + useMemoized( + () { + if (mapped case Fail(:final error)) { + // if callback tries to build widget(show snackbar for example) this will prevent exceptions + WidgetsBinding.instance.addPostFrameCallback( + (_) => failureCallBack.value?.call(error), + ); + } + if (mapped case Success()) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => successCallBack.value?.call(), + ); + } + }, + [mapped, failureCallBack.value, successCallBack.value], + ); + + return ( + state: mapped, + setFuture: (future) => mutationUpdate.value = future, + setOnFailure: (onFailure) => failureCallBack.value = onFailure, + ); +} diff --git a/lib/utils/bottom_sheet_page.dart b/lib/utils/bottom_sheet_page.dart new file mode 100644 index 00000000..efb118b3 --- /dev/null +++ b/lib/utils/bottom_sheet_page.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class BottomSheetPage extends Page { + const BottomSheetPage({ + required this.builder, + this.fixed = false, + }); + + final Widget Function(ScrollController? controller) builder; + final bool fixed; + + @override + Route createRoute(BuildContext context) { + return ModalBottomSheetRoute( + settings: this, + isScrollControlled: !fixed, + useSafeArea: true, + showDragHandle: true, + builder: (_) { + if (!fixed) { + return DraggableScrollableSheet( + expand: false, + builder: (_, scrollController) => builder(scrollController), + ); + } + return builder(null); + }, + ); + } +} diff --git a/lib/utils/callback_debouncer.dart b/lib/utils/callback_debouncer.dart new file mode 100644 index 00000000..4bcc1105 --- /dev/null +++ b/lib/utils/callback_debouncer.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +class CallbackDebouncer { + CallbackDebouncer(this._delay); + + final Duration _delay; + Timer? _timer; + + /// Calls the given [callback] after the given duration has passed. + void call(VoidCallback callback) { + if (_delay == Duration.zero) { + callback(); + } else { + _timer?.cancel(); + _timer = Timer(_delay, callback); + } + } + + /// Stops any running timers and disposes this instance. + void dispose() { + _timer?.cancel(); + } +} diff --git a/lib/utils/custom_loggers.dart b/lib/utils/custom_loggers.dart new file mode 100644 index 00000000..fabbab45 --- /dev/null +++ b/lib/utils/custom_loggers.dart @@ -0,0 +1,31 @@ +import 'package:loggy/loggy.dart'; + +/// application layer logger +/// +/// used in notifiers and controllers +mixin AppLogger implements LoggyType { + @override + Loggy get loggy => Loggy('🧮 $runtimeType'); +} + +/// presentation layer logger +/// +/// used in widgets and ui +mixin PresLogger implements LoggyType { + @override + Loggy get loggy => Loggy('🏰 $runtimeType'); +} + +/// data layer logger +/// +/// used in Repositories, DAOs, Services +mixin InfraLogger implements LoggyType { + @override + Loggy get loggy => Loggy('💾 $runtimeType'); +} + +abstract class LoggerMixin { + LoggerMixin(this.loggy); + + final Loggy loggy; +} diff --git a/lib/utils/custom_text_form_field.dart b/lib/utils/custom_text_form_field.dart new file mode 100644 index 00000000..01740b53 --- /dev/null +++ b/lib/utils/custom_text_form_field.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hiddify/utils/text_utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class CustomTextFormField extends HookConsumerWidget { + const CustomTextFormField({ + super.key, + required this.onChanged, + this.validator, + this.controller, + this.initialValue = '', + this.suffixIcon, + this.label, + this.hint, + this.maxLines = 1, + this.isDense = false, + this.autoValidate = false, + this.autoCorrect = false, + }); + + final ValueChanged onChanged; + final String? Function(String? value)? validator; + final TextEditingController? controller; + final String initialValue; + final Widget? suffixIcon; + final String? label; + final String? hint; + final int maxLines; + final bool isDense; + final bool autoValidate; + final bool autoCorrect; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textController = + controller ?? useTextEditingController(text: initialValue); + final effectiveConstraints = + isDense ? const BoxConstraints(maxHeight: 56) : null; + final effectiveBorder = isDense + ? OutlineInputBorder( + borderRadius: BorderRadius.circular(36), + borderSide: BorderSide.none, + ) + : null; + + return TextFormField( + controller: textController, + textCapitalization: TextCapitalization.sentences, + maxLines: maxLines, + onChanged: onChanged, + textDirection: textController.textDirection, + validator: validator, + autovalidateMode: + autoValidate ? AutovalidateMode.always : AutovalidateMode.disabled, + autocorrect: autoCorrect, + decoration: InputDecoration( + isDense: true, + label: label != null ? Text(label!) : null, + hintText: hint, + hintStyle: Theme.of(context).textTheme.bodySmall, + constraints: effectiveConstraints, + suffixIcon: suffixIcon, + border: effectiveBorder, + enabledBorder: effectiveBorder, + errorBorder: effectiveBorder, + focusedBorder: effectiveBorder, + focusedErrorBorder: effectiveBorder, + ), + ); + } +} diff --git a/lib/utils/link_parsers.dart b/lib/utils/link_parsers.dart new file mode 100644 index 00000000..59345ad1 --- /dev/null +++ b/lib/utils/link_parsers.dart @@ -0,0 +1,33 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/utils/validators.dart'; + +typedef ProfileLink = ({String url, String name}); + +// TODO: test and improve +abstract class LinkParser { + static const protocols = ['clash', 'clashmeta']; + + static ProfileLink? simple(String link) { + if (!isUrl(link)) return null; + final uri = Uri.parse(link); + final params = uri.queryParameters; + return ( + url: uri + .replace(queryParameters: {}) + .toString() + .removeSuffix('?') + .split('&') + .first, + name: params['name'] ?? '', + ); + } + + static ProfileLink? deep(String link) { + final uri = Uri.parse(link); + if (protocols.none((e) => uri.scheme == e)) return null; + if (uri.authority != 'install-config') return null; + final params = uri.queryParameters; + if (params['url'] == null) return null; + return (url: params['url']!, name: params['name'] ?? ''); + } +} diff --git a/lib/utils/mutation_state.dart b/lib/utils/mutation_state.dart new file mode 100644 index 00000000..b83f9a4c --- /dev/null +++ b/lib/utils/mutation_state.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'mutation_state.freezed.dart'; + +// TODO: remove +@freezed +class MutationState with _$MutationState { + const MutationState._(); + + const factory MutationState.initial() = MutationInitial; + const factory MutationState.inProgress() = MutationInProgress; + const factory MutationState.failure(Failure failure) = MutationFailure; + const factory MutationState.success() = MutationSuccess; + + bool get isInProgress => this is MutationInProgress; +} diff --git a/lib/utils/number_formatters.dart b/lib/utils/number_formatters.dart new file mode 100644 index 00000000..3156a3cc --- /dev/null +++ b/lib/utils/number_formatters.dart @@ -0,0 +1,22 @@ +import 'dart:math'; + +import 'package:intl/intl.dart'; + +const _units = ["B", "kB", "MB", "GB", "TB"]; + +({String size, String unit}) formatByteSpeed(int speed) { + const base = 1024; + if (speed <= 0) return (size: "0", unit: "B/s"); + final int digitGroups = (log(speed) / log(base)).round(); + return ( + size: NumberFormat("#,##0.#").format(speed / pow(base, digitGroups)), + unit: "${_units[digitGroups]}/s", + ); +} + +String formatTrafficByteSize(int consumption, int total) { + const base = 1024; + if (total <= 0) return "0 B / 0 B"; + final formatter = NumberFormat("#,##0.#"); + return "${formatter.format(consumption / pow(base, 3))} GB / ${formatter.format(total / pow(base, 3))} GB"; +} diff --git a/lib/utils/placeholders.dart b/lib/utils/placeholders.dart new file mode 100644 index 00000000..201ea194 --- /dev/null +++ b/lib/utils/placeholders.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +// TODO: improve +class SliverBodyPlaceholder extends HookConsumerWidget { + const SliverBodyPlaceholder(this.children, {super.key}); + + final List children; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ); + } +} + +class SliverLoadingBodyPlaceholder extends HookConsumerWidget { + const SliverLoadingBodyPlaceholder({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + ), + ); + } +} + +class SliverErrorBodyPlaceholder extends HookConsumerWidget { + const SliverErrorBodyPlaceholder(this.msg, {super.key, this.icon}); + + final String msg; + final IconData? icon; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon ?? Icons.error), + const Gap(16), + Text(msg), + ], + ), + ); + } +} diff --git a/lib/utils/platform_utils.dart b/lib/utils/platform_utils.dart new file mode 100644 index 00000000..1eff0dfb --- /dev/null +++ b/lib/utils/platform_utils.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +abstract class PlatformUtils { + static bool get isDesktop => + Platform.isLinux || Platform.isWindows || Platform.isMacOS; +} diff --git a/lib/utils/string_formatters.dart b/lib/utils/string_formatters.dart new file mode 100644 index 00000000..6736d83c --- /dev/null +++ b/lib/utils/string_formatters.dart @@ -0,0 +1,10 @@ +import 'package:duration/duration.dart'; + +// TODO: use a better solution +String formatExpireDuration(Duration dur) { + return prettyDuration( + dur, + upperTersity: DurationTersity.day, + tersity: DurationTersity.day, + ); +} diff --git a/lib/utils/text_utils.dart b/lib/utils/text_utils.dart new file mode 100644 index 00000000..a7660f22 --- /dev/null +++ b/lib/utils/text_utils.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' as intl; + +extension TextAlignX on BuildContext { + bool get isRtl => Directionality.of(this) == TextDirection.rtl; + + TextAlign get textAlign { + if (isRtl) { + return TextAlign.right; + } else { + return TextAlign.left; + } + } +} + +extension StringX on String { + TextDirection get textDirection { + return intl.Bidi.detectRtlDirectionality(this) + ? TextDirection.rtl + : TextDirection.ltr; + } +} + +extension TextEditingControllerX on TextEditingController { + TextDirection? get textDirection { + if (text.isEmpty) return null; + return text.textDirection; + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 00000000..a81a0c92 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,14 @@ +export 'alerts.dart'; +export 'async_mutation.dart'; +export 'bottom_sheet_page.dart'; +export 'callback_debouncer.dart'; +export 'custom_loggers.dart'; +export 'custom_text_form_field.dart'; +export 'link_parsers.dart'; +export 'mutation_state.dart'; +export 'number_formatters.dart'; +export 'placeholders.dart'; +export 'platform_utils.dart'; +export 'string_formatters.dart'; +export 'text_utils.dart'; +export 'validators.dart'; diff --git a/lib/utils/validators.dart b/lib/utils/validators.dart new file mode 100644 index 00000000..fff27f9b --- /dev/null +++ b/lib/utils/validators.dart @@ -0,0 +1,9 @@ +/// https://gist.github.com/dperini/729294 +final _urlRegex = RegExp( + r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$", +); + +/// https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url/3809435#3809435 +bool isUrl(String input) { + return _urlRegex.hasMatch(input.trim().toLowerCase()); +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /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 00000000..c168f747 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# 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 "hiddify") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.hiddify.hiddify") + +# 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) + +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 00000000..d5bd0164 --- /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 00000000..0e9e5827 --- /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) proxy_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin"); + proxy_manager_plugin_register_with_registrar(proxy_manager_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) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_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 00000000..e0f0a47b --- /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 00000000..792677d6 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + proxy_manager + screen_retriever + sqlite3_flutter_libs + 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 00000000..e7c5c543 --- /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 00000000..4fa82f94 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#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, "hiddify"); + 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, "hiddify"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(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, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /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/macos/.gitignore b/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /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 00000000..c2efd0b6 --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..c2efd0b6 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..2429992a --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,32 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_local_notifications +import mobile_scanner +import path_provider_foundation +import protocol_handler +import proxy_manager +import screen_retriever +import share_plus +import shared_preferences_foundation +import sqlite3_flutter_libs +import tray_manager +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ProtocolHandlerPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerPlugin")) + ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7da608da --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,695 @@ +// !$*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 */; }; +/* 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 = ( + ); + 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 /* hiddify.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "hiddify.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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* hiddify.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 = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 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 = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* hiddify.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + 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 */ + 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"; + }; +/* 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; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hiddify.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hiddify"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hiddify.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hiddify"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hiddify.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hiddify"; + }; + 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", + ); + 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", + ); + 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", + ); + 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 100644 index 00000000..18d98100 --- /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 100644 index 00000000..1030b590 --- /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 100644 index 00000000..1d526a16 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /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 100644 index 00000000..d53ef643 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + 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 100644 index 00000000..a2ec33f1 --- /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 100644 index 00000000..82b6f9d9 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 100644 index 00000000..13b35eba 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 100644 index 00000000..0a3f5fa4 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 100644 index 00000000..bdb57226 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 100644 index 00000000..f083318e 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 100644 index 00000000..326c0e72 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 100644 index 00000000..2f1632cf Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /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 100644 index 00000000..1c8080d4 --- /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 = hiddify + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.hiddify + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /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 100644 index 00000000..dff4f495 --- /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 100644 index 00000000..42bcbf47 --- /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 100644 index 00000000..dddb8a30 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + 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 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +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() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..5418c9f5 --- /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/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..dcd0149a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1479 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" + url: "https://pub.dev" + source: hosted + version: "60.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" + url: "https://pub.dev" + source: hosted + version: "5.12.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "30859c90e9ddaccc484f56303931f477b1f1ba2bab74aa32ed5d6ce15870f8cf" + url: "https://pub.dev" + source: hosted + version: "7.2.8" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "2f17434bd5d52a26762043d6b43bb53b3acd029b4d9071a329f46d67ef297e6d" + url: "https://pub.dev" + source: hosted + version: "8.5.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + color: + dependency: transitive + description: + name: color + sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + combine: + dependency: "direct main" + description: + name: combine + sha256: "2474dab27a4d7fb52ecad28cca696f60b42422a2e1579367bd95b3ae772131ce" + url: "https://pub.dev" + source: hosted + version: "0.5.3" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" + source: hosted + version: "0.17.2" + csv: + dependency: transitive + description: + name: csv + sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "3ce36c04d30c60cde295588c6185b3f9800e6c18f6670a7ffdb3d5eab39bb942" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: "73d09c9848e9f6d5c3e0a1809eac841a8d7ea123d0849feefa040e1ad60b6d06" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "9170d9db2daf774aa2251a3bc98e4ba903c7702ab07aa438bc83bd3c9a0de57f" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" + dartx: + dependency: "direct main" + description: + name: dartx + sha256: "45d7176701f16c5a5e00a4798791c1964bc231491b879369c818dd9a9c764871" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: a9d76e72985d7087eb7c5e7903224ae52b337131518d127c554b9405936752b8 + url: "https://pub.dev" + source: hosted + version: "5.2.1+1" + drift: + dependency: "direct main" + description: + name: drift + sha256: a8ec4e44b4359ef44eab3d2c2f8e44b41a00c15673b879984484b34d27656ad5 + url: "https://pub.dev" + source: hosted + version: "2.9.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "2713aabc91d8e9cdf269b2ecfa503f103341925b07186e845de11a781015f7eb" + url: "https://pub.dev" + source: hosted + version: "2.9.0" + duration: + dependency: "direct main" + description: + name: duration + sha256: d0b29d0a345429e3986ac56d60e4aef65b37d11e653022b2b9a4b361332b777f + url: "https://pub.dev" + source: hosted + version: "3.0.12" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: "direct main" + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a + url: "https://pub.dev" + source: hosted + version: "8.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432 + url: "https://pub.dev" + source: hosted + version: "5.3.2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592 + url: "https://pub.dev" + source: hosted + version: "0.63.0" + flex_color_scheme: + dependency: "direct main" + description: + name: flex_color_scheme + sha256: "5be124fa61821a684c0628ca5f687a8512db06738f784516278688eb727a3ef1" + url: "https://pub.dev" + source: hosted + version: "7.1.2" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: e4168a6fc88a3e5bc3d6b7a748c6a6083eedc193d343ddc26bbad7fb1b258555 + url: "https://pub.dev" + source: hosted + version: "1.3.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: "01ee32a8f64b68f15436b5e59124e79df9c46bbffe7bbb6edb8ae93d879511fb" + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: be54662837a6e66cc53ee88549e808c625275e0faf5a43e11cf3182cb0bd1b02 + url: "https://pub.dev" + source: hosted + version: "4.2.0" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + sha256: e8637dd6a59860f89e5e71be0a27101ec32dad1a0ed7fd879fd23b6e91d5004d + url: "https://pub.dev" + source: hosted + version: "5.3.1" + flutter_gen_runner: + dependency: "direct dev" + description: + name: flutter_gen_runner + sha256: "7de1bf4fc0439be0fef3178b6423d5c7f1f9f3a38a7c6fafe75d7f70ff4856d7" + url: "https://pub.dev" + source: hosted + version: "5.3.1" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + url: "https://pub.dev" + source: hosted + version: "0.18.6" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: c4602f3047b383e6b652f3edb23bb35a2d33fec65c7b8cadaedc9e8ff5c48893 + url: "https://pub.dev" + source: hosted + version: "15.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_loggy: + dependency: "direct main" + description: + name: flutter_loggy + sha256: "21b515977deefe37817cce35b0e420c7cde930b9dcfdcbeb05730ed24ee74e3a" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + sha256: ba45d8cfbd778478a74696b012f33ffb6b1760c9bc531b21e2964444a4870dae + url: "https://pub.dev" + source: hosted + version: "2.3.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + url: "https://pub.dev" + source: hosted + version: "2.3.6" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + 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" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + fpdart: + dependency: "direct main" + description: + name: fpdart + sha256: "4a0d047c3359a4bdd19e4941603bbe394bd133b3772a38049e17f9534d47106e" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 + url: "https://pub.dev" + source: hosted + version: "2.3.5" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: db02ec4ac4511ea8d324d7f671d526959a8e7857468b4ea64113fe8a82f16a2c + url: "https://pub.dev" + source: hosted + version: "2.0.2" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "1531542666c2d052c44bbf6e2b48011bf3771da0404b94c60eabec1228a62906" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + go_router_builder: + dependency: "direct dev" + description: + name: go_router_builder + sha256: dbd5028908e5e1409ffc3da02257a489f6ac1caa509a0e03f126b1c2e451bb65 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: be68cf7653fcab798500f9047ac58c3f109287a1595012412f4a0d654a9bb9c5 + url: "https://pub.dev" + source: hosted + version: "2.3.6" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + html: + dependency: transitive + description: + name: html + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" + url: "https://pub.dev" + source: hosted + version: "0.15.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + icons_launcher: + dependency: "direct main" + description: + name: icons_launcher + sha256: "71cf6baae0f0d1752e3c26035f9e6ea07bd297af72d389e907fb0119e9d56ba1" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + image: + dependency: transitive + description: + name: image + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + url: "https://pub.dev" + source: hosted + version: "4.0.17" + intl: + dependency: "direct main" + description: + name: intl + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" + source: hosted + version: "0.18.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json2yaml: + dependency: transitive + description: + name: json2yaml + sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + lint: + dependency: "direct dev" + description: + name: lint + sha256: f4bd4dbaa39f4ae8836f2d1275f2f32bc68b3a8cce0a0735dd1f7a601f06682a + url: "https://pub.dev" + source: hosted + version: "2.1.2" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + loggy: + dependency: "direct main" + description: + name: loggy + sha256: "981e03162bbd3a5a843026f75f73d26e4a0d8aa035ae060456ca7b30dfd1e339" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + meta: + dependency: "direct main" + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54dd914e1bb5758b3db7aa02e56d65d80285ba0705e0e5fa5cfbb11e27344c4b" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" + source: hosted + version: "2.1.7" + path_to_regexp: + dependency: transitive + description: + name: path_to_regexp + sha256: "169d78fbd55e61ea8873bcca545979f559d22238f66facdd7ef30870c7f53327" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + url: "https://pub.dev" + source: hosted + version: "4.2.3" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + protocol_handler: + dependency: "direct main" + description: + name: protocol_handler + sha256: be85285f5cdc2541bb5ae5368f1c9924eeaae8d28e74c7235b7cd2c02521d760 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + proxy_manager: + dependency: "direct main" + description: + name: proxy_manager + sha256: "4cdb8853bcedc1a6879c6d940d624d740e1c76ee6b83377b13270f96a8415a37" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" + recase: + dependency: "direct main" + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "1b2632a6fc0b659c923a4dcc7cd5da42476f5b3294c70c86c971e63bdd443384" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: cedd6a54b6f5764ffd5c05df57b6676bfc8c01978e14ee60a2c16891038820fe + url: "https://pub.dev" + source: hosted + version: "2.1.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: cd0595de57ccf5d944ff4b0f68289e11ac6a2eff1e3dfd1d884a43f6f3bcee5e + url: "https://pub.dev" + source: hosted + version: "2.2.3" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "043ff016011be4c5887b3239bfbca05d284bdb68db0a5363cee0242b7567e250" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: ed3fcea4f789ed95913328e629c0c53e69e80e08b6c24542f1b3576046c614e8 + url: "https://pub.dev" + source: hosted + version: "7.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + slang: + dependency: "direct main" + description: + name: slang + sha256: "187e35d220765ff22be030b30506d1b8e0e94842a2c1801a6a2941c95db5a9eb" + url: "https://pub.dev" + source: hosted + version: "3.19.0" + slang_build_runner: + dependency: "direct dev" + description: + name: slang_build_runner + sha256: "3c48c91736704879b767552bf9e7ba38f1974dd06f44b5e15981cadfde06d760" + url: "https://pub.dev" + source: hosted + version: "3.19.0" + slang_flutter: + dependency: "direct main" + description: + name: slang_flutter + sha256: c6c58162ef66fe88be0313d8062a39e98ae9b539dde7b35f59fa206eb4db2030 + url: "https://pub.dev" + source: hosted + version: "3.19.0" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c + url: "https://pub.dev" + source: hosted + version: "0.2.10" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: f7511ddd6a2dda8ded9d849f8a925bb6020e0faa59db2443debc18d484e59401 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "1e20a88d5c7ae8400e009f38ddbe8b001800a6dffa37832481a86a219bc904c7" + url: "https://pub.dev" + source: hosted + version: "0.5.15" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "0d2c9a3b554baa10b2560d69a1c7cabd4687cc08041a7dd3d2dc6992f607b400" + url: "https://pub.dev" + source: hosted + version: "0.30.0" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" + source: hosted + version: "0.7.2+1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + time: + dependency: transitive + description: + name: time + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: "4addcda362e51f23cf7ae2357fccd053f29d59b4ddd17fb07fc3e7febb47a456" + url: "https://pub.dev" + source: hosted + version: "3.5.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + url: "https://pub.dev" + source: hosted + version: "2.0.17" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f3743ca475e0c9ef71df4ba15eb2d7684eecd5c8ba20a462462e4e8b561b2e11 + url: "https://pub.dev" + source: hosted + version: "11.6.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "1414f27dd781737e51afa9711f2ac2ace6ab4498ee98e20863fa5505aa00c58c" + url: "https://pub.dev" + source: hosted + version: "5.0.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + url: "https://pub.dev" + source: hosted + version: "2.1.1" +sdks: + dart: ">=3.0.1 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..36273907 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,135 @@ +name: hiddify +description: A Proxy Frontend. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.0.1 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.5 + + # internationalization + flutter_localizations: + sdk: flutter + intl: ^0.18.0 + slang: ^3.19.0 + slang_flutter: ^3.19.0 + recase: ^4.1.0 + timeago: ^3.5.0 + duration: ^3.0.12 + + # data & serialization + fpdart: ^0.6.0 + freezed_annotation: ^2.2.0 + json_annotation: ^4.8.1 + + # state management + hooks_riverpod: ^2.3.6 + flutter_hooks: ^0.18.6 + riverpod_annotation: ^2.1.1 + rxdart: ^0.27.7 + + # persistence + drift: ^2.9.0 + sqlite3_flutter_libs: ^0.5.15 + shared_preferences: ^2.2.0 + + # networking + dio: ^5.2.1+1 + web_socket_channel: ^2.4.0 + + # native + ffi: ^2.0.2 + proxy_manager: ^0.0.3 + path_provider: ^2.0.15 + flutter_local_notifications: ^15.0.1 + mobile_scanner: ^3.3.0 + protocol_handler: ^0.1.5 + flutter_native_splash: ^2.3.1 + icons_launcher: ^2.1.2 + file_picker: ^5.3.2 + share_plus: ^7.0.2 + window_manager: ^0.3.5 + tray_manager: ^0.2.0 + + # utils + combine: ^0.5.3 + path: ^1.8.3 + loggy: ^2.0.3 + flutter_loggy: ^2.0.2 + meta: ^1.9.1 + stack_trace: ^1.11.0 + dartx: ^1.1.0 + uuid: ^3.0.7 + + # widgets + go_router: ^9.0.0 + flex_color_scheme: ^7.1.2 + flutter_animate: ^4.2.0 + flutter_svg: ^2.0.7 + gap: ^2.0.2 + percent_indicator: ^4.2.3 + fluttertoast: ^8.2.2 + sliver_tools: ^0.2.10 + flutter_adaptive_scaffold: ^0.1.5 + fl_chart: ^0.63.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.1.2 + build_runner: ^2.4.6 + json_serializable: ^6.7.1 + freezed: ^2.3.5 + riverpod_generator: ^2.2.3 + drift_dev: ^2.9.0 + ffigen: ^8.0.2 + slang_build_runner: ^3.19.0 + flutter_gen_runner: ^5.3.1 + go_router_builder: ^2.2.0 + custom_lint: ^0.4.0 + riverpod_lint: ^1.3.2 + +flutter: + uses-material-design: true + assets: + - assets/core/clash/ + - assets/images/ + +flutter_gen: + output: lib/gen/ + integrations: + flutter_svg: true + +icons_launcher: + image_path: "assets/images/ic_launcher.png" + platforms: + android: + enable: true + adaptive_background_image: "assets/images/ic_launcher_background.png" + adaptive_foreground_image: "assets/images/ic_launcher_foreground.png" + adaptive_round_image: "assets/images/ic_launcher_round.png" + adaptive_monochrome_image: "assets/images/ic_launcher_monochrome.png" + windows: + enable: true + image_path: "assets/images/ic_launcher_round.png" + +flutter_native_splash: + color: "#ffffff" + image: assets/images/ic_launcher.png + android_12: + image: assets/images/ic_launcher_foreground.png + color: "#ffffff" + +ffigen: + name: 'ClashNativeLibrary' + description: 'Bindings to Clash' + output: 'lib/gen/clash_generated_bindings.dart' + headers: + entry-points: + - 'core/dist/libclash.h' + +scripts: scripts/derry.yaml \ No newline at end of file diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +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/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 00000000..f3d30a28 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,105 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(hiddify 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 "hiddify") + +# 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) + +install(FILES "../core/dist/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 00000000..930d2071 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# 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") + +# === 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" + windows-x64 $ + 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 00000000..0ee8c2fb --- /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) { + ProtocolHandlerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ProtocolHandlerPlugin")); + ProxyManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ProxyManagerPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /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 00000000..39c9d456 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,31 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + protocol_handler + proxy_manager + screen_retriever + share_plus + sqlite3_flutter_libs + tray_manager + url_launcher_windows + window_manager +) + +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/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /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 00000000..d9acb3eb --- /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.example" "\0" + VALUE "FileDescription", "hiddify" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "hiddify" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "hiddify.exe" "\0" + VALUE "ProductName", "hiddify" "\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 00000000..b25e363e --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#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([&]() { + this->Show(); + }); + + 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 00000000..6da0652f --- /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 00000000..654e7e6d --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,53 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +#include + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"hiddify"); + if (hwnd != NULL) { + DispatchToProtocolHandler(hwnd); + + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + return EXIT_FAILURE; + } + // 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"hiddify", 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 00000000..66a65d1e --- /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 00000000..c4137def 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 00000000..a42ea768 --- /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 00000000..b2b08734 --- /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 00000000..3879d547 --- /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 00000000..60608d0f --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.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) { + 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); +} + +// 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 00000000..e901dde6 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#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; + + // 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_